Skip to content
This repository has been archived by the owner on Dec 19, 2017. It is now read-only.

Data Binding Syntax

Jacob MacDonald edited this page Oct 5, 2015 · 7 revisions

Data binding binds a property or sub-property of a custom element (the host element) to a property or attribute of an element in its local DOM (the child or target element).

A binding is created with a binding annotation in the host element's local DOM template:

<dom-module id="host-element">
  <template>
    <child-element name="{{myName}}"></child-element>  
  </template>
</dom-module>

Binding annotations

A binding annotation consists of a property name or subproperty name enclosed in curly brackets ({{}}) or square brackets ([[]]).

  • Square brackets ([[]]) create one-way bindings. Data flow is downward, host-to-child, and the binding never modifies the host property.

  • Curly brackets ({{}}) create automatic bindings. Data flow is one-way or two-way, depending whether the target property is configured for two-way binding.

To bind to a child property, specify the attribute name that corresponds to the property, with an annotation as the attribute value:

<child-element name="{{myName}}"></child-element>  

This example binds the child element's name property to the host element's myName property.

While HTML attributes are used to specify bindings, values are assigned directly to JavaScript properties, not to the HTML attributes of the elements. (There is a special attribute binding syntax for those cases where you want to bind to an attribute value.)

Attribute names are mapped to property names as described in Property name to attribute name mapping. To bind to camel-case properties of elements, use dash-case in the attribute name. For example:

<user-view first-name="{{managerName}}"></user-view>
<!-- Sets <user-view>.firstName = this.managerName; -->

Some attributes are special. When binding to style, href, class, for or data-* attributes, it is recommend that you use attribute binding syntax. For more information, see Binding to native element attributes.

Binding to text content

To bind to a child element's textContent, you can simply include the annotation inside the child element. The binding annotation must currently span the entire content of the tag:

user_view.html:

<dom-module id="user-view">
    <template>   
      First: <span>{{firstName}}</span><br>
      Last: <span>{{lastName}}</span>
    </template>
</dom-module>

user_view.dart:

@PolymerRegister('user-view')
class UserView extends PolymerElement {
  UserView.created() : super.created();
  
  @property
  String firstName;
  
  @property
  String lastName;
}

String concatenation is not supported inside a tag, and the tag can't contain any whitespace:

<!-- Not currently supported! -->
<div>First: {{firstName}}</div>
<div>Last: {{lastName}}</div>

<!-- Not currently supported! -->
<div>
  {{title}}
</div>

Binding to text content is always one-way, host-to-child.

Binding to sub-properties

Binding annotations can also include paths to sub-properties, as shown below:

main_view.html:

<dom-module id="main-view">
  <template>
    <user-view first-name="{{user.first}}" last-name="{{user.last}}"></user-view>
  </template>
</dom-module>

main_view.dart:

@PolymerRegister('main-view')
class MainView extends PolymerElement {
  MainView.created() : super.created();
  
  @property
  User user;
}

class User extends JsProxy {
  @reflectable
  String first;

  @reflectable
  String last;

  User(this.first, this.last);
}

See Binding to structured data for details.

Property change notification and two-way binding

Polymer supports cooperative two-way binding between elements, allowing elements that "produce" data or propagate data changes upwards to hosts when desired.

To avoid two-way binding, use "square-brace" syntax ([[property]]), which results in only one-way (downward, host-to-child) data-binding.

To summarize, two-way data-binding is achieved when both the host and the child agree to participate, satisfying these three conditions:

  1. The host must use curly-brace {{property}} syntax. (Square-brace [[property]] syntax results in one-way downward binding, regardless of how the child property is configured.)

  2. The child property being bound to must be configured with the notify flag set to true (or otherwise send a <property>-changed custom event). (If the property being bound does not have the notify flag set, only one-way (downward) binding will occur.)

  3. The child property being bound to must have a public setter. If the child property is notify: true, only has a getter, and the host binding uses curly-brace syntax, the binding is one-way, upward (child-to-host).

Example 1: Two-way binding

custom_element.dart:

@PolymerRegister('custom-element')
class CustomElement extends PolymerElement {
  CustomElement.created() : super.created();
  
  @Property(notify: true);
  String someProp;
}

host.html:

    <!-- changes to "value" propagate downward to "someProp" on child -->
    <!-- changes to "someProp" propagate upward to "value" on host  -->
    <custom-element some-prop="{{value}}"></custom-element>

Example 2: One-way binding (downward)

custom_element.dart:

@PolymerRegister('custom-element')
class CustomElement extends PolymerElement {
  CustomElement.created() : super.created();
  
  @Property(notify: true);
  String someProp;
}

host.html:

<!-- changes to "value" propagate downward to "someProp" on child -->
<!-- changes to "someProp" are ignored by host due to square-bracket syntax -->
<custom-element some-prop="[[value]]"></custom-element>

Example 3: One-way binding (downward)

custom_element.dart:

@PolymerRegister('custom-element')
class CustomElement extends PolymerElement {
  CustomElement.created() : super.created();
  
  @property         // no notify:true!
  String someProp;
}

host.html:

<!-- changes to "value" propagate downward to "someProp" on child -->
<!-- changes to "someProp" are not notified to host due to notify:false -->
<custom-element some-prop="{{value}}"></custom-element>

Example 4: One-way binding (upward, child-to-host)

custom_element.dart:

@PolymerRegister('custom-element')
class CustomElement extends PolymerElement {
  CustomElement.created() : super.created();
  
  String _someProp;
  
  @Property(notify: true);
  String get someProp => _someProp;
}

host.html:

<!-- changes to "value" are ignored by child due to it only having a getter -->
<!-- changes to "someProp" propagate upward to "value" on host  -->
<custom-element some-prop="{{value}}"></custom-element>

Example 5: Error / non-sensical state

custom_element.dart:

@PolymerRegister('custom-element')
class CustomElement extends PolymerElement {
  CustomElement.created() : super.created();
  
  String _someProp;
  
  @Property(notify: true);
  String get someProp => _someProp;
}

host.html:

<!-- changes to "value" are ignored by child due to readOnly:true -->
<!-- changes to "someProp" are ignored by host due to square-bracket syntax -->
<!-- binding serves no purpose -->
<custom-element some-prop="[[value]]"></custom-element>

Change notification protocol

When you configure a declared property with the notify flag set to true, Polymer propagates data changes upward by firing events:

  • When the property changes, the element fires a non-bubbling DOM event to indicate those changes to interested hosts.

  • The event follows a naming convention of property-changed, and contains a value property in the event.detail object indicating the property's new value.

When using a Polymer element with other elements or frameworks, you can manually attach an on-property-changed listener to an element to be notified of property changes, and take the necessary actions based on the new value.

This is essentially what Polymer does when you create a two-way data binding.

Two-way binding to native elements

As mentioned above, Polymer uses an event naming convention to achieve two-way binding.

To two-way bind to native elements or non-Polymer elements that don't follow this event naming convention, you can specify a custom change event name in the annotation using the following syntax:

target-prop="{{hostProp::target-change-event}}"

Example:

<!-- Listens for `input` event and sets hostValue to <input>.value -->
<input value="{{hostValue::input}}">

<!-- Listens for `change` event and sets hostChecked to <input>.checked -->
<input type="checkbox" checked="{{hostChecked::change}}">

<!-- Listens for `timeupdate ` event and sets hostTime to <video>.currentTime -->
<video url="..." current-time="{{hostTime::timeupdate}}">

Note: When binding to standard notifying properties on Polymer elements, specifying the event name is unnecessary, as the default convention will be used. The following constructions are equivalent:

<!-- Listens for `value-changed` event -->
<my-element value="{{hostValue::value-changed}}">

<!-- Listens for `value-changed` event using Polymer convention by default -->
<my-element value="{{hostValue}}">

Binding to structured data

You can bind sub-properties of the host by specifying a path inside the binding annotation.

Example:

<template>
  <div>{{user.manager.name}}</div>
  <user-element user="{{user}}"></user-element>
</template>

The path syntax doesn't support array-style accessors (such as users[0].name). However, you can include indexes directly in the path (users.0.name).

For a path binding to update, the path value must be updated in one of the following ways:

  • Using a Polymer property binding to another element.

  • Using the set API, which provides the required notification to elements with registered interest.

Note: These requirements are identical to the requirements for sub-property change observers, which use the same notification system.

Path bindings are distinct from property bindings in a subtle way:

  • When a property value changes, the host element assigns the new value to the bound property on the child element.

  • When two elements are bound to the same path of a shared object and the value at that path changes, the new value is immediately visible to both elements, because both elements are accessing the same object.

    In this case, the element that changed the path must notify the system so that other elements that have registered interest in the same path may take side effects. Path bindings notify the system automatically. Imperative code must call set.

Path bindings are two-way. There is no concept of one-way bindings for paths, since the data does not propagate. That is, all bindings and change handlers for the same path will always be notified and update when the value of the path changes.

Path change notification

Two-way data-binding and observation of paths in Polymer is achieved using a similar strategy to the one described above for 2-way property binding:

  1. When a sub-property of a property changes, an element fires a non-bubbling <property>-changed DOM event with a detail[path] value indicating the path on the object that changed.

  2. Elements that have registered interest in that object (either via binding or change handler) may then take the appropriate action.

  3. Finally, those elements will forward the notification on to any children they have bound the object to, and will also fire a new <property>-changed event where property is the root object, to notify any hosts that may have bound root object down.

This way, a notification reaches any part of the tree that has registered interest in that path so that side effects occur.

This system "just works" to the extent that changes to object sub-properties occur as a result of being bound to a notifying custom element property that changed. However, sometimes imperative code needs to change an object's sub- properties directly. As we avoid more sophisticated observation mechanisms such as Object.observe or dirty-checking in order to achieve the best startup and runtime performance cross-platform for the most common use cases, changing an object's sub-properties directly requires cooperation from the user.

Specifically, Polymer provides two methods that allow such changes to be notified to the system: notifyPath(path, value) and set(path, value), where path is a String identifying the path (relative to the host element).

Example:

custom_element.html:

<dom-module id="custom-element">
  <template>
    <div>{{user.manager.name}}</div>
  </template>
</dom-module>

custom_element.dart:

@PolymerRegister('custom-element')
class CustomElement extends PolymerElement {
  CustomElement.created() : super.created();
  
  @property
  User user;
  
  void reassignManager(newManager) {
    user.manager = newManager;
    // Notification required for binding to update!
    this.notifyPath('user.manager', user.manager);
  }
}

Most of the time, notifyPath is called directly after an assignment, so a convenience function set is provided that performs both actions:

void reassignManager(newManager) {
  this.set('user.manager', newManager);
}

Note: Paths do not support array access notation (such as users[2]). String keys (such as users[bob]) can be replaced with dotted paths (users.bob). But direct bindings to array items by index ({{array.0}}) isn't supported. See Binding to array items.

Expressions in binding annotations

Currently there's no general support for expressions in binding annotations. The two exceptions are:

  • Negation using !:

    Example:

<template>
  <div hidden="{{!enabled}}"></div>
</template>

Computed bindings

For more complicated bindings, you can use a computed binding. A computed binding is similar to a computed property: it includes a computing function and zero or more arguments. Arguments can be dependent properties or string or number literals.

A computed binding is useful if you don't need to expose a computed property as part of the element's API, or use it elsewhere in the element.

Note: this is the only form of function allowed in binding annotations.

Example:

x_custom.html:

<dom-module id="x-custom">
  <template>
    My name is <span>{{computeFullName(first, last)}}</span>
  </template>
</dom-module>

x_custom.dart:

@PolymerRegister('x-custom')
class XCustom extends PolymerElement {
  XCustom.created() : super.created();
  
  @property
  String first;
  
  @property
  String last;
  
  @reflectable
  String computeFullName(String first, String last) => '$first $last';
}

In this case, the span's textContent property is bound to the return value of computeFullName, which is recalculated whenever first or last changes.

Dependent properties in computed bindings

Arguments to computing functions may be dependent properties, which include any of argument types supported by the @Observe object:

For each type of dependent property, the argument received by the computing function is the same as that passed to an observer.

The computing function is not called until all dependent properties are defined. So each dependent properties should be initialized to a non-null value) to ensure the function value is computed.

A computed binding's dependent properties are interpreted relative to the current binding scope. For example, inside a template repeater, a dependent property could refer to the current item.

For an example of computed binding using a path with a wildcard, see Binding to array items.

Literal arguments to computed bindings

Arguments to computed bindings may also be string or number literals. Strings may be either single- or double-quoted. In an attribute or property binding, if you use double quotes for the attribute value, use single quotes for string literals, or the reverse.

Commas in literal strings: Any comma occurring in a string literal must be escaped using a backslash (\).

Example:

<dom-module id="x-custom">
  <template>
    <span>{{translate('Hello\, nice to meet you', first, last)}}</span>
  </template>
</dom-module>

Finally, if a computed binding has no dependent properties, it is only evaluated once:

x_custom.html:

    <dom-module id="x-custom">
      <template>
        <span>{{doThisOnce()}}</span>
      </template>
    </dom-module>

x_custom.dart:

import 'dart:math';

@PolymerRegister('x-custom')
class XCustom extends PolymerElement {
  XCustom.created() : super.created();

  @reflectable
  double doThisOnce() => new Random().nextDouble();
}

Binding to array items

Explicit bindings to array items by index isn't supported:

<!-- don't do this! -->
<span>{{array[0]}}</span>
<!-- or this! -->
<span>{{array.0}}</span>

You can use a computed binding to bind to a specific array item, or to a subproperty of an array item, like array[index].name.

The following example shows to access a property from an array item using a computed binding. The computing function needs to be called if the subproperty value changes, or if the array itself is mutated, so the binding uses a wildcard path, myArray.*.

bind_array_element.html:

<dom-module id="bind-array-element">
  <template>
    <div>[[arrayItem(myArray.*, 0, 'name')]]</div>
    <div>[[arrayItem(myArray.*, 1, 'name')]]</div>
  </template>
</dom-module>

bind_array_element.dart:

@PolymerRegister('bind-array-element')
class BindArrayElement extends PolymerElement {
  BindArrayElement.created() : super.created();

  @property
  List myArray = [new User('Bob'), new User('Doug')];

  // first argument is the change record for the array change,
  // change['base'] is the array specified in the binding
  @reflectable
  String arrayItem(Map change, int index, String path) {
    // get(path, root) returns a value for a path relative to a root object.
    return get(path, change['base'][index]);
  }

  void ready() {
    // mutate the array
    insert('myArray', 0, new User('Susan'));
    // change a subproperty
    set('myArray.1.name', 'Rupert');
  }
}

Annotated attribute binding

In the vast majority of cases, binding data to other elements should use property binding described above, where changes are propagated by setting the new value to the JavaScript property on the element.

However, sometimes you need to set an attribute on an element, as opposed to a property. These include when attribute selectors are used for CSS or for interoperability with elements that require using an attribute-based API.

To bind to an attribute, use $= rather than =. This results in a call to:

element.setAttribute(attr, value);

As opposed to:

element.property = value;

<template>
  <!-- Attribute binding -->
  <my-element selected$="{{value}}"></my-element>
  <!-- results in <my-element>.setAttribute('selected', this.value); -->

  <!-- Property binding -->
  <my-element selected="{{value}}"></my-element>
  <!-- results in <my-element>.selected = this.value; -->
</template>

Attribute bindings are always one-way, host-to-child. Values are serialized according to the value's current type, as described for attribute serialization.

Again, as values must be serialized to strings when binding to attributes, it is always more performant to use property binding for pure data propagation.

Binding to native element attributes

There are a handful of extremely common native element attributes which can also be modified as properties. Due to cross-browser limitations with the ability to place binding braces {{...}} in some of these attribute values, as well as the fact that some of these attributes map to differently named JavaScript properties, it is recommended to always use attribute binding (using $=) when binding dynamic values to these specific attributes, rather than binding to their property names.

Normal attribute assignment to static values:

<!-- class -->
<div class="foo"></div>

<!-- style -->
<div style="background: red;"></div>

<!-- href -->
<a href="http://foo.com">

<!-- label for -->
<label for="bar"></label>

<!-- dataset -->
<div data-bar="baz"></div>

Attribute binding to dynamic values (use $=):

    <!-- class -->
<div class$="{{foo}}"></div>

<!-- style -->
<div style$="{{background}}"></div>

<!-- href -->
<a href$="{{url}}">

<!-- label for -->
<label for$="{{bar}}"></label>

<!-- dataset -->
<div data-bar$="{{baz}}"></div>