AMP

Important: this documentation is not applicable to your currently selected format stories!

amp-script

Description

Runs custom JavaScript in a Web Worker.

 

Required Scripts

<script async custom-element="amp-script" src="https://cdn.ampproject.org/v0/amp-script-0.1.js"></script>

Usage

The amp-script component allows you to run custom JavaScript. To maintain AMP's performance guarantees, your code runs in a Web Worker, and certain restrictions apply.

Virtual DOM

Your JavaScript can access the area of the page wrapped within the <amp-script> component. amp-script copies the component's children to a virtual DOM. Your code can access that virtual DOM as document.body.

For example, this <amp-script> component defines a DOM consisting of a single <p>.

<amp-script src="http://example.com/my-script.js" width="300" height="100">
  <p>A single line of text</p>
</amp-script>

If your code appends an element to document.body:

// my-script.js
const p = document.createElement('p');
p.textContent = 'A second line of text';
document.body.appendChild(p);

the new element will be placed after that <p>.

<amp-script src="http://example.com/my-script.js" width="300" height="100">
  <p>A single line of text</p>
  <p>A second line of text</p>
</amp-script>

Loading JavaScript

An amp-script element can load JavaScript in two ways:

  • remotely, from a URL
  • locally, from a <script> element on the page

From a remote URL

Use the src attribute to load JavaScript from a URL:

<amp-script layout="container" src="https://example.com/hello-world.js">
  <button>Hello amp-script!</button>
</amp-script>

From a local element

You can also include your JavaScript inline, in a script tag. You must:

  • Set the script attribute of your amp-script to the local script element's id.
  • Include target="amp-script" in your amp-script.
  • Include type="text/plain" in your script. This way, the browser won't execute your script, allowing amp-script to control it.
<!-- To use inline JavaScript, you must add a script hash to the document head. -->
<head>
  <meta
    name="amp-script-src"
    content="sha384-YCFs8k-ouELcBTgzKzNAujZFxygwiqimSqKK7JqeKaGNflwDxaC3g2toj7s_kxWG"
  />
</head>

...

<amp-script width="200" height="100" script="hello-world">
  <button>Hello amp-script!</button>
</amp-script>

<!-- Add [target="amp-script"] to the <script> element. -->
<script id="hello-world" type="text/plain" target="amp-script">
  const btn = document.querySelector('button');
  btn.addEventListener('click', () => {
    document.body.textContent = 'Hello World!';
  });
</script>

For security reasons, amp-script elements with a script or cross-origin src attribute require a script hash in a <meta name="amp-script-src" content="..."> tag. Also, same-origin src files must have Content-Type: application/javascript or text/javascript.

If your page contains multiple amp-script elements, each requiring a script hash, include each as a whitespace-delimited list in a single <meta name="amp-script-src" content="..."> tag (see examples/amp-script/example.amp.html for an example with multiple script hashes).

How does it work?

amp-script runs your custom JavaScript in a Web Worker. Normally Web Workers don't have access to the DOM. But amp-script gives your code access to a virtual DOM. When your JavaScript modifies this virtual DOM, amp-script updates the real DOM.

Under the hood, amp-script uses @ampproject/worker-dom. For design details, see the "Intent to Implement" issue.

Capabilities

Supported APIs

DOM elements and their properties are generally supported, with a few limits. For example, your code can't add a new <script> or <style> tag to the DOM.

amp-script recreates many commonly used DOM APIs and makes them available to your code. This "hello world" example uses getElementById(), addEventListener(), createElement(), textContent, and appendChild():

const button = document.getElementById('textarea');

button.addEventListener('click', () => {
  const h1 = document.createElement('h1');
  h1.textContent = 'Hello World!';
  document.body.appendChild(h1);
});

Supported DOM APIs include:

  • Element getters like getElementByName(), getElementsByClassName(), getElementsByTagName(), childNodes(), parentNode(), and lastChild()
  • Mutators like createTextNode(), appendChild(), insertBefore(), removeChild(), and replaceChild()
  • Methods involving events like addEventListener(), removeEventListener(), and createEvent()
  • Property and attribute getters like getAttribute(), hasAttribute()
  • Event properties like Event.target, Event.type, and Event.bubbles
  • Element properties like attributes, id, outerHTML, textContent, value, classList, and className
  • And many more.

For a complete list of supported DOM APIs, see the API compatibility table.

querySelector() is supported for simple selectors - element, id, class, and attribute. So, document.querySelector('.class') will work, but document.querySelector('.class1 .class2') will not. See the code for details.

amp-script supports common Web APIs like Fetch, WebSockets, localStorage, sessionStorage, and Canvas. Presently, the History API is not implemented, and neither are cookies.

amp-script does not support the entire DOM API or Web API, as this would make amp-script's own JavaScript too large and slow. If there's an API you'd like to see supported, please file an issue or suggest and contribute the change yourself.

For a set of samples showing amp-script in use, see here.

Frameworks and libraries

Presently, libraries like jQuery will not work with amp-script without modification, as they use unsupported DOM APIs. However, @ampproject/worker-dom was designed to support the APIs that popular JavaScript frameworks use. amp-script has been tested with React and Preact. To keep bundle sizes small, we recommend using Preact. Other frameworks may work but have not been thoroughly tested; if you're looking for support, please file an issue or contribute here.

Creating AMP elements

You can use amp-script to add an amp-img or amp-layout component to the DOM. Other AMP components are presently unsupported. If you need to create a different AMP element, please upvote on #25344 and add a comment describing your use case.

Referencing amp-state

amp-script supports getting and setting amp-state JSON. This lets amp-script interact with other AMP elements on the page via amp-bind bindings.

If you invoke AMP.setState() from amp-script after a user gesture, bindings can cause mutations to the DOM. Otherwise, state will be set, but binding will not occur (similar to amp-state initialization). For more information, see the section on user gestures.

AMP.setState() works as described in the <amp-bind> documentation, merging an object literal into the specified state. This example shows how it can affect the DOM:

<script id="myscript" type="text/plain" target="amp-script">
  const button = document.getElementsByTagName('button')[0];

  function changer() {
    AMP.setState({myText: "I have changed!"});
  }

  button.addEventListener('click', changer);
</script>

<amp-script layout="container" script="myscript">
  <p [text]="myText">Will I change?</p>
  <button>Change it!</button>
</amp-script>

AMP.getState() is asynchronous and returns a Promise. The promise resolves with the stringified value of the state variable it's passed. These examples demonstrate its use with different types:

async function myFunction() {
  AMP.setState({'text': 'I am a string'});
  let text = await AMP.getState('text');

  AMP.setState({'number': 42});
  let number = Number(await AMP.getState('number'));

  AMP.setState({'obj': {'text': 'I am a string', 'number': 42}});
  let obj = JSON.parse(await AMP.getState('obj'));
}

Here's another example. amp-state doesn't support WebSocket URLs in its src attribute, but we can use amp-script to pass in data from a WebSocket.

<amp-script width="1" height="1" script="webSocketDemo"> </amp-script>

<script type="text/plain" target="amp-script" id="webSocketDemo">
  const socket = new WebSocket('wss://websocket.example');
  socket.onmessage = event => {
    AMP.setState({socketData: event.data});
  };
</script>

To use AMP.setState(), you must include the amp-bind extension script in the document head.

Note that if something else changes the DOM inside your <amp-script>, that change will not propagate to the virtual DOM. The syncing process is unidirectional. Thus it's best to avoid code like the following:

<amp-script layout="container" script="myscript">
  <p [text]="myText">Will I change?</p>
</amp-script>
<button on="tap:AMP.setState({myText: 'I changed'})">
  Change this and amp-script won't know
</button>

Retrieving data for <amp-list>

You may export a function to act as the data source for an <amp-list>. The exported function must return either JSON, or a Promise that resolves with JSON.

The export API is available on the global scope and has the following signature:

/**
 * @param {string} name the name to identify the function by.
 * @param {Function} function the function to export.
 */
function exportFunction(name, function) {}

Restrictions

In order to maintain AMP's guarantees of performance and layout stability, amp-script imposes some restrictions.

Size of JavaScript code

amp-script has standards for the size of code:

  • Each inline script can contain up to 10,000 bytes
  • The scripts on a page can contain a total of up to 150,000 bytes
  • The scripts running in sandboxed mode on a page can contain a total of up to 300,000 bytes

User gestures

In some cases, amp-script will not apply DOM changes triggered by your JavaScript code unless the code was triggered by a user gesture - say, a button tap. This helps keep content layout shift from causing a poor user experience.

The rules are less restrictive for amp-script containers whose size cannot change. This is the case when the layout is not container and when the dimensions are specified in HTML. Let's call such containers "fixed-size". Containers whose size can change, we'll call "variable-sized".

Here are some examples of fixed-size containers:

<amp-script layout="fill" height="300" width="500" script="myscript"></amp-script>

<amp-script layout="fixed-height" height="300" script="myscript" ></amp-script>

Here are some examples of variable-sized containers:

<amp-script layout="responsive" script="myscript"></amp-script>

<amp-script layout="fixed" height="300" script="myscript"></amp-script>

<amp-script layout="container" height="300" width="500" script="myscript"></amp-script>

DOM changes are permitted as follows:

  • in fixed-size containers, your code can make any change at any time.
  • in variable-size containers, your code can only make a change after a user gesture. It then has 5 seconds to make changes. If your code makes one or more fetch()'s, it can continue to make changes until 5 seconds after the last fetch() completes.
fixed-size containervariable-size container
change on page loadallowed anytimenot allowed
change after user eventallowed anytimeallowed for 5 seconds + fetch()

See here for more information about AMP's layout system.

Calculating the script hash

Since custom JS run in amp-script is not subject to normal Content Security Policy, you need to add a script hash:

  • for inline JavaScript
  • for JavaScript loaded from a cross-origin source

Include the script hash in a meta[name=amp-script-src] element in the document head. You need a hash for each script that's used by an <amp-script> component. Here are a few ways to build the hash:

  • If you omit the <meta> tag, AMP will output a console error containing the expected hash string. You can copy this to create the appropriate <meta> tag.
  • The AMP Optimizer node module generates this hash and inserts the <meta> tag automatically.
  • Build it yourself, using the following steps:
  1. Compute the SHA384 hash sum of the script's contents. This sum should be expressed in hexadecimal.
  2. base64url-encode the result.
  3. Prefix that with sha384-.

Here's how you calculate the hash in Node.js:

const crypto = require('crypto');
const hash = crypto.createHash('sha384');

function generateCSPHash(script) {
  const data = hash.update(script, 'utf8');
  return (
    'sha384-' +
    data
      .digest('base64')
      .replace(/=/g, '')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
  );
}

The @ampproject/toolbox-script-csp node module computes the hash as well.

This example shows how to use the script hash in HTML:

<head>
  <!--
    A meta[name="amp-script-src"] element contains all script hashes for
    <amp-script> elements on the page, delimited by spaces.
  -->
  <meta
    name="amp-script-src"
    content="
      sha384-fake_hash_of_remote_js
      sha384-fake_hash_of_local_script
    "
  />
</head>
<body>
  <!--
    A "src" attribute with a cross-origin URL requires adding a script hash.

    If the hash of remote.js's contents is "fake_hash_of_remote_js",
    we'll add "sha384-fake_hash_of_remote_js" to the <meta> tag above.

-->
  <amp-script src="cross.origin/remote.js" layout="container"> </amp-script>

  <!--
    A "script" attribute also requires adding a script hash.

    If the hash of #myScript's text contents is "fake_hash_of_local_script",
    we'll add "sha384-fake_hash_of_local_script" to the <meta> tag above.
  -->
  <amp-script script="myScript" layout="container"> </amp-script>
  <script type="text/plain" target="amp-script" id="myScript">
    document.body.textContent += 'Hello world!';
  </script>
</body>