Important: this documentation is not applicable to your currently selected format stories!
amp-bind
Description
Allows elements to mutate in response to user actions or data changes via data binding and simple JS-like expressions.
Required Scripts
<script async custom-element="amp-bind" src="https://cdn.ampproject.org/v0/amp-bind-0.1.js"></script>
Usage
The amp-bind
component enables custom stateful interactivity on AMP pages.
For performance, and to avoid the risk of unexpected content jumping, amp-bind
does not evaluate expressions on page load. This means visual elements should be given a default state and not rely on amp-bind
for initial render.
amp-bind
has three main concepts:
- State: A document-scope, mutable JSON state. State variables update in response to user actions.
amp-bind
does not evaluate expressions on page load. Visual elements should have their default "state" defined and not relyamp-bind
for initial render. - Expressions: JavaScript-like expressions that can reference the state.
- Bindings: Special attributes that link an element's property to a state via an expression. A property is bound by wrapping it inside brackets, in the form of
[property]
.
Example without declared state
Hello World
<p [text]="'Hello ' + foo">Hello World</p> <button on="tap:AMP.setState({foo: 'Interactivity'})"> Say "Hello Interactivity" </button>
In the example above:
- The state begins as empty.
- It has a single binding to
[text]
, the text content of a node, on the<p>
element. - The
[text]
value contains the expression,'Hello ' + foo
. This expression concatenates the string 'Hello ' and the value of the state variable foo.
When the user taps/clicks the button:
- It triggers the
tap
event. - The
tap
event invokes theAMP.setState()
method. - The
AMP.setState()
methods sets thefoo
state variable to the value ofInteractivity
. - The state is no longer empty, so the page updates the bound property to its state.
AMP.setState()
in some examples may set or change states of other examples on page. Refresh this page to see examples before AMP.setState()
. Example with declared state
<amp-state>
specification
State
Each AMP document that uses amp-bind
has document-scope mutable JSON data, or state.
Size
An <amp-state>
element's JSON data has a maximum size of 100KB.
Defining and initializing state with <amp-state>
Expressions are not evaluated on page load, but you may define an initial state. The <amp-state>
component contains different states and their state variables. While this defines a state, it will not reflect on the page until after a user interacts.
<amp-state id="myDefinedState"> <script type="application/json"> { "foo": "bar" } </script> </amp-state> <p [text]="myDefinedState.foo"></p> <button on="tap:AMP.setState({})">See value of initialized state</button>
Use expressions to reference state variables. If the JSON data is not nested in the <amp-state>
component, reference the states via dot syntax. In the above example, myState.foo
evaluates to "bar".
An <amp-state>
element can also specify a CORS URL instead of a child JSON script. See the <amp-state>
specification for details.
<amp-state id="myRemoteState" src="/static/samples/json/websites.json"> </amp-state>
Updating state variables with AMP.setState()
The AMP.setState()
action merges an object literal into the state. This means you can update the value of a defined state variable.
<amp-state id="myUpdateState"> <script type="application/json"> { "foo": "bar", "baz": "hello" } </script> </amp-state> <p [text]="myUpdateState.foo"></p> <p [text]="myUpdateState.baz"></p> <button on="tap:AMP.setState({})">See value of set state</button> <!-- Like JavaScript, you can reference existing variables in the values of the object literal. --> <button on="tap:AMP.setState({myUpdateState:{baz: myUpdateState.foo}})"> Set value of baz to value of foo </button> <button on="tap:AMP.setState({myUpdateState:{baz: 'world'}})"> Set value of baz to "world" </button>
In the example above, triggering the AMP.setState({})
action on the first button evaluates the [text]
binding expression. It then inserts the defined state variable's value into the <p>
tag.
When the clicking the second button, with AMP.setState({myState:{baz: myState.foo}})
action defined, it deep-merges the "baz" state variable value to the same as the "foo" state variable value. Both <p>
tags display "bar".
State variable values can update to values not defined in the initial state. When clicking the third button, with "tap:AMP.setState({myState:{baz: 'world'}})"
action defined, it deep merges the "baz" state variable value, overriding it to "world".
Clicking the first button after the other two sets the current state. Nothing will change.
The state variables reverts back to the defined JSON in <amp-state>
on page refresh.
Event triggering and data
When triggered by certain events, AMP.setState()
can access event-related data on the event
property.
<!-- The "change" event of this <input> element contains a "value" variable that can be referenced via "event.value". --> <select on="change:AMP.setState({ option: event.value })"> <option value="0">No selection</option> <option value="1">Option 1</option> <option value="2">Option 2</option> </select> <div hidden [hidden]="option != 1"> Option 1 </div> <div hidden [hidden]="option != 2"> Option 2 </div>
Updating nested variables
Nested objects are generally merged to a maximum depth of 10. All variables, including those defined in <amp-state>
, can be overidden.
<amp-state id="myState"> <script type="application/json"> { "foo": "bar", "first": { "a": "nested once", "ab": { "b": "nested twice", "bc": { "c": "nested three times", "cd": { "d": "nested four times", "de": { "e": "nested five times", "ef": { "f": "nested six times", "fg": { "g": "nested seven times", "gh": { "h": "nested nine times", "hi": { "i": "nested ten times" } } } } } } } } } } </script> </amp-state> <p [text]="myState.foo"></p> <p [text]="myState.first.ab.bc.cd.de.ef.fg.gh.hi.i"></p> <button on="tap:AMP.setState({})">See value of set state</button> <button on="tap:AMP.setState({ myState: {first: {ab: {bc: {cd: {de: {ef: {fg: {gh: {hi: {i:'this is as far as you should merge nested values'} } } } } } } } } } })" > Merge 10th nested object </button>
Circular references
AMP.setState(object)
throws an error if object
contains a circular reference.
Removing a variable
Remove an existing state variable by setting its value to null
in AMP.setState()
.
<button on="tap:AMP.setState({removeMe: null})"></button>
Deep-merge with AMP.setState()
Calling AMP.setState()
deep-merges the provided object literal with the current state. amp-bind
writes all literals to the state directly, except for nested objects, which are recursively merged. Primitives and arrays are in the state are always overwritten by variables of the same name in the object literal.
Name
Age
Vehicle
<p [text]="employee.name">Name</p> <p [text]="employee.age">Age</p> <p [text]="employee.vehicle">Vehicle</p> <!-- Pressing this button changes state to: --> <button on="tap:AMP.setState({ employee: { name: 'John Smith', age: 47, vehicle: 'Car' } })" > Set employee to John Smith </button> <!-- Pressing this button recursively merges the object literal argument, --> <!-- `{employee: {age: 64}}`, into the existing state. --> <button on="tap:AMP.setState({ employee: { age: 64 } })" > Set employee age to 64 </button> <!-- The value updates from 47 to 64 at employee.age. --> <!-- No other values change. -->
Expressions
amp-bind
uses JavaScript-like expressions that can reference the state.
Differences from JavaScript
- Expressions may only access the containing document's state.
- Expressions do not have access to
window
ordocument
.global
references the top-level state. - Only
amp-bind
allowlisted functions and operators are usable. are usable. Use of arrow functions are allowed as function parameters, e.g.[1, 2, 3].map(x => x + 1)
.- Custom functions, classes and loops are disallowed.
- Undefined variables and array-index-out-of-bounds return
null
instead ofundefined
or throwing errors. - A single expression is currently capped at 250 operands for performance. Please contact us if this is insufficient for your use case.
The following are all valid expressions:
<p [text]="myExpressionsState.foo"></p> <!-- 1 + '1'; // 11 --> <button on="tap:AMP.setState({myExpressionsState: {foo: 1 + '1'}})"> foo: 1 + "1" </button> <!-- 1 + +'1'; // 2 --> <button on="tap:AMP.setState({myExpressionsState: {foo: 1 + + '1'}})"> foo: 1 + + "1" </button> <!-- !0; // true --> <button on="tap:AMP.setState({myExpressionsState: {foo: !0}})">foo: !0</button> <!-- null || 'default'; // 'default' --> <button on="tap:AMP.setState({myExpressionsState: {foo: null || 'default'}})"> null || "default" </button> <!-- [1, 2, 3].map(x => x + 1); // 2,3,4 --> <button on="tap:AMP.setState({myExpressionsState: {foo: [1, 2, 3].map(x => x + 1)}})" > [1, 2, 3].map(x => x + 1) </button>
Find the full expression grammar and implementation in bind-expr-impl.jison and bind-expression.js.
Allowlisted functions
Array
Single-parameter arrow functions can't have parentheses, e.g. use x => x + 1
instead of (x) => x + 1
. sort()
and splice()
return modified copies instead of operating in-place.
concat: 1, 2, 3
word.length > 3)" i-amphtml-binding> filter: words with less than three letter
includes: "hello" or "world"
indexOf: "world"
join: all words with a dash
lastIndexOf: "amp-bind"
x + i)" i-amphtml-binding> map: add each number to previous number
x + i)" i-amphtml-binding> reduce: add all numbers in array together
slice: return words at index 1 and 3
x < 2)" i-amphtml-binding> some: some numbers are less than 2
sort: place words in alphabetical order
splice: place "amp-bind" at index 2
<amp-state id="myArrayState"> <script type="application/json"> { "foo": [1, 2, 3], "bar": ["hello", "world", "bar", "baz"], "baz": "Hello world, welcome to amp-bind" } </script> </amp-state> <p [text]="'concat: ' + myArrayState.foo.concat(4)">concat: 1, 2, 3</p> <p [text]="'filter: ' + myArrayState.bar.filter(word => word.length > 3)"> filter: words with less than three letter </p> <p [text]="'includes: ' + myArrayState.bar.includes('hello' || 'world')"> includes: "hello" or "world" </p> <p [text]="'indexOf: ' + myArrayState.bar.indexOf('world')">indexOf: "world"</p> <p [text]="'join: ' + myArrayState.bar.join('-')"> join: all words with a dash </p> <p [text]="'lastIndexOf: ' + myArrayState.baz.lastIndexOf('amp-bind')"> lastIndexOf: "amp-bind" </p> <p [text]="'map: ' + myArrayState.foo.map((x, i) => x + i)"> map: add each number to previous number </p> <p [text]="'reduce: ' + myArrayState.foo.reduce((x, i) => x + i)"> reduce: add all numbers in array together </p> <p [text]="'slice: ' + myArrayState.bar.slice(1,3)"> slice: return words at index 1 and 3 </p> <p [text]="'some: ' + myArrayState.foo.some(x => x < 2)"> some: some numbers are less than 2 </p> <p [text]="'sort: ' + myArrayState.bar.sort()"> sort: place words in alphabetical order </p> <p [text]="'splice: ' + myArrayState.bar.splice(2, 0, 'amp-bind')"> splice: place "amp-bind" at index 2 </p> <button on="tap:AMP.setState({})">Evaluate</button>
Number
toExponential: 100 to the exponent of 5
toFixed: 1.99 rounded and fixed to first decimal
toPrecision: 1.234567 returned as a string to the third digit
toString: 3.14 returned as a string
<p [text]="'toExponential: ' + (100).toExponential(5)"> toExponential: 100 to the exponent of 5 </p> <p [text]="'toFixed: ' + (1.99).toFixed(1)"> toFixed: 1.99 rounded and fixed to first decimal </p> <p [text]="'toPrecision: ' + (1.234567).toPrecision(3)"> toPrecision: 1.234567 returned as a string to the third digit </p> <p [text]="'toString ' + (3.14).toString()"> toString: 3.14 returned as a string </p> <button on="tap:AMP.setState({})">Evaluate</button>
String
charAt: The character at index 6
charCodeAt: The UTF-16 code unit of the character at index 6
concat: Combine foo and bar
lastIndexOf: The index of "w"
replace: Replace "world" with "amp-bind"
slice: Extract the first 5 characters
split: Split words at space and return as array
toLowerCase: Make all letters lower case
toUpperCase: Make all letters upper case
<amp-state id="myStringState"> <script type="application/json"> { "foo": "Hello world", "bar": ", welcome to amp-bind" } </script> </amp-state> <p [text]="'charAt: ' + myStringState.foo.charAt(6)"> charAt: The character at index 6 </p> <p [text]="'charCodeAt: ' + myStringState.foo.charCodeAt(6)"> charCodeAt: The UTF-16 code unit of the character at index 6 </p> <p [text]="'concat: ' + myStringState.foo.concat(myState.bar)"> concat: Combine foo and bar </p> <p [text]="'lastIndexOf: ' + myStringState.foo.lastIndexOf('w')"> lastIndexOf: The index of "w" </p> <p [text]="'replace: ' + myStringState.foo.replace('world', 'amp-bind')"> replace: Replace "world" with "amp-bind" </p> <p [text]="'slice: ' + myStringState.foo.slice(5)"> slice: Extract the first 5 characters </p> <p [text]="'split: ' + myStringState.foo.split(' ')"> split: Split words at space and return as array </p> <p [text]="'toLowerCase: ' + myStringState.foo.toLowerCase()"> toLowerCase: Make all letters lower case </p> <p [text]="'toUpperCase: ' + myStringState.foo.toUpperCase()"> toUpperCase: Make all letters upper case </p> <button on="tap:AMP.setState({})">Evaluate</button>
Math
Static functions are not namespaced, e.g. use abs(-1)
instead of Math.abs(-1)
abs: absolute number of 5 - 9
abs: round 1.01 up to the next largest whole number
floor: round 1.99 down to a whole number
max: return largest number
min: return smalled number
pow: return 5 to the power of 3
random: return a number greater than 0 and less than 1
round: round 1.51
sign: evaluate if positive or negative
<p [text]="'abs: ' + abs(5 - 9)">abs: absolute number of 5 - 9</p> <p [text]="'ceil: ' + ceil(1.01)"> abs: round 1.01 up to the next largest whole number </p> <p [text]="'floor: ' + floor(1.99)">floor: round 1.99 down to a whole number</p> <p [text]="'max: ' + max(100, 4, 98)">max: return largest number</p> <p [text]="'min: ' + min(100, 4, 98)">min: return smalled number</p> <p [text]="'pow: ' + pow(5, 3)">pow: return 5 to the power of 3</p> <p [text]="'random: ' + random()"> random: return a number greater than 0 and less than 1 </p> <p [text]="'round: ' + round(1.51)">round: round 1.51</p> <p [text]="'sign: ' + sign(-9)">sign: evaluate if positive or negative</p> <button on="tap:AMP.setState({})">Evaluate</button>
Object
Static functions are not namespaced, e.g. use keys(Object)
instead of Object.abs(Object)
keys: myObjectState JSON object keys
values: myObjectState JSON object values
<amp-state id="myObjectState"> <script type="application/json"> { "hello": "world", "foo": "bar" } </script> </amp-state> <p [text]="'keys: ' + keys(myObjectState)"> keys: myObjectState JSON object keys </p> <p [text]="'values: ' + values(myObjectState)"> values: myObjectState JSON object values </p> <button on="tap:AMP.setState({})">Evaluate</button>
Global
encodeURI: Encode a URI and ignore protocol prefix
encodeURIComponent: Encode a URI
<p [text]="'encodeURI: ' + encodeURI('https://amp.dev/😉')"> encodeURI: Encode a URI and ignore protocol prefix </p> <p [text]="'encodeURIComponent: ' + encodeURIComponent('https://amp.dev/😉')"> encodeURIComponent: Encode a URI </p> <button on="tap:AMP.setState({})">Evaluate</button>
Defining macros with amp-bind-macro
Reuse amp-bind
expression fragments by defining an amp-bind-macro
. The amp-bind-macro
element allows an expression that takes zero or more arguments and references the current state. Invoke amp-bind-macros
like a function, referencing the id
attribute value from anywhere in the document.
Input a radius value
The circle has an area of 0.
<amp-bind-macro id="circleArea" arguments="radius" expression="3.14 * radius * radius" ></amp-bind-macro> <p> Input a radius value </p> <input type="number" min="0" max="100" value="0" on="input-throttled:AMP.setState({myCircle:{radius: event.value}})" /> <p> The circle has an area of <span [text]="circleArea(myCircle.radius)">0</span>. </p>
A macro can also call other macros defined before itself. A macro cannot call itself recursively.
Bindings
A binding is a special attribute of the form [property]
that links an element's property to an expression. Use the alternative,XML-compatible syntax if developing in XML.
When the state changes, expressions tied to that state are evaluated. The element properties bound to the state are updated with the new expression results.
Boolean expression results toggle boolean attributes. For example: <amp-video [controls]="expr"...>
. When expr
evaluates to true
, the <amp-video>
element has the controls
attribute. When expr
evaluates to false
, the controls
attribute is removed.
<amp-video [controls]="controls" width="640" height="360" layout="responsive" poster="/static/inline-examples/images/kitten-playing.png" > <source src="/static/inline-examples/videos/kitten-playing.webm" type="video/webm" /> <source src="/static/inline-examples/videos/kitten-playing.mp4" type="video/mp4" /> <div fallback> <p>This browser does not support the video element.</p> </div> </amp-video> <button on="tap:AMP.setState({ controls: true })"> Controls </button> <button on="tap:AMP.setState({ controls: false })"> No Controls </button>
React and XML compatibility
If developing with React or XML, use the alternative data-amp-bind-property
syntax. The [
and ]
characters in attribute names is invalid XML, making the [property]
syntax unavailable.
Replace the property
field with the name of the property you would like to define in data-amp-bind-property
.
For example, [text]="myState.foo"
would become data-amp-bind-text="myState.foo"
.
Binding types
amp-bind
supports data bindings on five types of element state.
Bind Node.textContent
using the [text]
attribute. The [text]
attribute is supported on most text elements.
<p [text]="'Hello ' + myState.foo">Hello World</p> <p></p>
CSS classes
Bind an element's class
using the [class]
attribute. A [class]
expression must result in a space-delimited string. Meaning, if you are binding multiple classes, use a space between names. A comma or dash will be evaluated as the class name.
<head> <style amp-custom> .background-green { background: green; } .background-red { background: red; } .border-red { border-color: red; border-width: 5px; border-style: solid; } </style> </head> <body> <div class="background-red" [class]="myClass">Hello World</div> <!-- This button adds both classes --> <button on="tap:AMP.setState({ myClass: 'background-green border-red' })"> Working: Change Class </button> <!-- String arrays also work --> <button on="tap:AMP.setState({ myClass: ['background-green', 'border-red'] })" > Working string array: Change Class </button> <!-- This expression evaluates to class="background-green,border-red" --> <button on="tap:AMP.setState({ myClass: 'background-green,border-red' })"> Broken: Change Class </button> </body>
Hide and reveal and element using the [hidden]
attribute. A [hidden]
expression should be a boolean expression.
Hello there!
<p [hidden]="hiddenState">Hello there!</p> <button on="tap:AMP.setState({hiddenState: true})">Hide</button> <button on="tap:AMP.setState({hiddenState: false})">Show</button>
Size of AMP components
Change the width
and height
using the [width]
and [height]
attributes.
<amp-img src="https://unsplash.it/400/200" width="200" [width]="myImageDimension.width" height="100" [height]="myImageDimension.height" > </amp-img> <button on="tap:AMP.setState({ myImageDimension: { width: 400, height: 200 } })" > Change size </button>
Accessibility states and properties
Use to dynamically update information available to assistive technologies, such as screen readers. All [aria-*]
attributes are bindable.
AMP Component specific and HTML attributes
Some AMP components and HTML elements have specific bindable attributes. They are listed below.
AMP component specific attributes
<amp-carousel type=slides>
[slide]
Changes the currently displayed slide index.
<amp-lightbox>
[open]
Toggles display of the lightbox.
on="lightboxClose: AMP.setState(...)"
to update variables when the lightbox is closed.
<details>
[open]
See corresponding details attributes.
<fieldset>
[disabled]
Enables or disables the fieldset.
<image>
[xlink:href]
See corresponding image attributes.
<option>
[disabled]
[label]
[selected]
[value]
See corresponding option attributes.
<optgroup>
[disabled]
[label]
See corresponding optgroup attributes.
<section>
[data-expand]
Changes the expansion of asection
in anamp-accordion
.
Disallowed bindings
For security reasons, binding to innerHTML
is disallowed.
All attribute bindings are sanitized for unsafe values (e.g., javascript:
).
Debugging
Warnings
In development mode, amp-bind
will issue a warning when the default value of a bound attribute doesn't match its corresponding expression's initial result. This can help prevent unintended mutations caused by changes in other state variables. For example:
<!-- The element's default class value ('def') doesn't match the expression result for [class] ('abc'), so a warning will be issued in development mode. --> <p [class]="'abc'" class="def"></p>
In development mode, amp-bind
will also issue a warning when dereferencing undefined variables or properties. This can also help prevent unintended mutations due to null
expression results. For example:
<amp-state id="myAmpState"> <script type="application/json"> {"foo": 123} </script> </amp-state> <!-- The amp-state#myAmpState does not have a `bar` variable, so a warning will be issued in development mode. --> <p [text]="myAmpState.bar">Some placeholder text.</p>
Errors
Below outlines the types of errors that may arise when working with amp-bind
.
Type | Message | Suggestion |
---|---|---|
Invalid binding | Binding to [foo] on <P> is not allowed. | Use only allowlisted bindings. |
Syntax error | Expression compilation error in... | Verify the expression for typos. |
Non-allowlisted functions | alert is not a supported function. | Use only allow-listed functions. |
Sanitized result | "javascript:alert(1)" is not a valid result for [href]. | Avoid banned URL protocols or expressions that would fail the AMP Validator. |
CSP violation | Refused to create a worker from 'blob:...' because it violates the following Content Security Policy directive... | Add default-src blob: to your origin's Content Security Policy. amp-bind delegates expensive work to a dedicated Web Worker to ensure good performance. |
Debugging State
Use AMP.printState()
to print the current state to the console. To make this work, you need to enable the development mode.
Expression grammar
The BNF-like grammar for amp-bind
expressions:
expr:
operation
| invocation
| member_access
| '(' expr ')'
| variable
| literal
;
operation:
'!' expr
| '-' expr %prec UMINUS
| '+' expr %prec UPLUS
| expr '+' expr
| expr '-' expr
| expr '*' expr
| expr '/' expr
| expr '%' expr
| expr '&&' expr
| expr '||' expr
| expr '<=' expr
| expr '<' expr
| expr '>=' expr
| expr '>' expr
| expr '!=' expr
| expr '==' expr
| expr '?' expr ':' expr
;
invocation:
NAME args
| expr '.' NAME args
| expr '.' NAME '(' arrow_function ')'
| expr '.' NAME '(' arrow_function ',' expr ')'
;
arrow_function:
'(' ')' '=>' expr
| NAME '=>' expr
| '(' params ')' '=>' expr
;
params:
NAME ',' NAME
| params ',' NAME
;
args:
'(' ')'
| '(' array ')'
;
member_access:
expr member
;
member:
'.' NAME
| '[' expr ']'
;
variable:
NAME
;
literal:
primitive
| object_literal
| array_literal
;
primitive:
STRING
| NUMBER
| TRUE
| FALSE
| NULL
;
array_literal:
'[' ']'
| '[' array ']'
| '[' array ',' ']'
;
array:
expr
| array ',' expr
;
object_literal:
'{' '}'
| '{' object '}'
| '{' object ',' '}'
;
object:
key_value
| object ',' key_value
;
key_value:
key ':' expr
;
key:
NAME
| primitive
| '[' expr ']'
;
Вы читали этот документ десятки раз, но так и не нашли ответов на все свои вопросы? Возможно, другие люди столкнулись с такой же проблемой — обратитесь к ним на Stack Overflow.
Перейти на Stack Overflow Нашли ошибку или недостающую функцию?Проект AMP активно поощряет ваше участие и сотрудничество! Мы надеемся, что вы станете постоянным участником нашего открытого сообщества, но разовые вклады в работу над задачами, которые вам особенно интересны, также приветствуются.
Перейти на GitHub