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

Data Binding Helper Elements

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

Polymer provides a set of custom elements to help with common data binding use cases:

  • Template repeater. Creates an instance of the template's contents for each item in an array.
  • Array selector. Manages selection state for an array of structured data.
  • Conditional template. Stamps its contents if a given condition is true.
  • Auto-binding template. Allows data binding outside of a Polymer element.

Template repeater (dom-repeat)

The template repeater is a specialized template that binds to an array. It creates one instance of the template's contents for each item in the array. It adds two properties to the binding scope for each instance:

  • item. The array item used to create this instance.
  • index. The index of item in the array. (The index value changes if the array is sorted or filtered)

The template repeater is a type-extension custom element that extends the built-in <template> element, so it is written as <template is="dom-repeat">.

Example:

employee_list.html:

<dom-module id="employee-list">
  <template>
    <div> Employee list: </div>
    <template is="dom-repeat" items="{{employees}}">
        <div># <span>{{index}}</span></div>
        <div>First name: <span>{{item.first}}</span></div>
        <div>Last name: <span>{{item.last}}</span></div>
    </template>
  </template>
</dom-module>

employee_list.dart:

@PolymerRegister('employee-list')
class EmployeeList extends PolymerElement {
  EmployeeList.created() : super.created();
  
  @property
  List<Employee> employees;
  
  void ready() {
    set('employees', [
      new Employee('Bob', 'Smith'),
      new Employee('Sally', 'Johnson'),
    ]);
  }
}

class Employee extends JsProxy {
  @reflectable
  String first;
 
  @reflectable
  String last;

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

Notifications for changes to items sub-properties are forwarded to the template instances, which update via the normal structured data notification system .

Mutations to the items list itself must be performed using methods provided on Polymer elements, such that the changes are observable to any elements bound to the same list in the tree. For example:

add('employees', new Employee('Jack', 'Aubrey'));

Handling events in dom-repeat templates

When handling events generated by a dom-repeat template instance, you frequently want to map the element firing the event to the model data that generated that item.

When you add a declarative event handler inside the <dom-repeat> template, the repeater adds a model property to each event sent to the listener. The model is the scope data used to generate the template instance, so the item data is model.item:

simple_menu.html:

<dom-module id="simple-menu">

  <template>
    <template is="dom-repeat" id="menu" items="{{menuItems}}">
        <div>
          <span>{{item.name}}</span>
          <span>{{item.ordered}}</span> 
          <button on-click="order">Order</button>
        </div>
    </template>
  </template>

</dom-module>

simple_menu.dart:

@PolymerRegister('simple-menu')
class SimpleMenu extends PolymerElement {
  SimpleMenu.created() : super.created();

  @property
  List<MenuItem> menuItems;

  void ready() {
    set('menuItems', [
      new MenuItem('Pizza'),
      new MenuItem('Pasta'),
      new MenuItem('Toast'),
    ]);
  }

  @reflectable
  void order(e, [_]) {
    var model = new DomRepeatModel.fromEvent(e);
    model.set('item.ordered', model.item.ordered + 1);
  }
}

class MenuItem extends JsProxy {
  @reflectable
  String name;

  @reflectable
  int ordered = 0;

  MenuItem(this.name);
}

The model is an instance of the js Polymer.Base object, so set, get and the js array manipulation methods are all available on the model object, and should be used to manipulate the model.

Note: The model property is not added for event listeners registered imperatively (using addEventListener, or on), or listeners added to one of the <dom-repeat> template's parent nodes. In these cases, you can use the <dom-repeat> modelForElement method to retrieve the model data that generated a given element. (There are also corresponding itemForElement and indexForElement methods.)

Filtering and sorting lists

To filter or sort the displayed items in your list, specify a filter or sort property on the dom-repeat (or both):

  • filter. Specifies a filter callback function following the standard Array filter API.
  • sort. Specifies a comparison function following the standard Array sort API.

In both cases, the value can be either a function object, or a string identifying a function defined on the host element.

By default, the filter and sort functions only run when the list itself is mutated (for example, by adding or removing items).

To re-run the filter or sort functions when certain sub-fields of items change, set the observe property to a space-separated list of item sub-fields that should cause the list to be re-filtered or re-sorted.

For example, for a dom-repeat with a filter of the following:

@reflectable
isEngineer(item) => item.type == 'engineer' || item.manager.type == 'engineer';

Then the observe property should be configured as follows:

<template is="dom-repeat" items="{{employees}}"
          filter="isEngineer" observe="type manager.type">

Changing a manager.type field should now cause the list to be re-sorted:

set('employees.0.manager.type', 'engineer');

Nesting dom-repeat templates

When nesting multiple dom-repeat templates, you may want to access data from a parent scope. Inside a dom-repeat, you can access any properties available to the parent scope unless they're hidden by a property in the current scope.

For example, the default item and index properties added by dom-repeat hide any similarly-named properties in a parent scope.

To access properties from nested dom-repeat templates, use the as attribute to assign a different name for the item property. Use the index-as attribute to assign a different name for the index property.

<div> Employee list: </div>
<template is="dom-repeat" items="{{employees}}" as="employee">
    <div>First name: <span>{{employee.first}}</span></div>
    <div>Last name: <span>{{employee.last}}</span></div>

    <div>Direct reports:</div>

    <template is="dom-repeat" items="{{employee.reports}}" as="report" index-as="report_no">
      <div><span>{{report_no}}</span>. 
           <span>{{report.first}}</span> <span>{{report.last}}</span>
      </div>
    </template>
</template>

Array selector

Keeping structured data in sync requires that Polymer understand the path associations of data being bound. The array-selector element ensures path linkage when selecting specific items from an array. The array selector supports either single or multiple selection.

The items property accepts an array of user data. Call select(item) and deselect(item) to update the selected property, which may be bound to other parts of the application. Any changes to sub-fields of the selected item(s) are kept in sync with items in the items array.

When multi is false, selected is a property representing the last selected item. When multi is true, selected is an array of selected items.

employee_list.html:

<dom-module id="employee-list">
  <template>
    <div> Employee list: </div>
    <template is="dom-repeat" id="employeeList" items="{{employees}}">
        <div>First name: <span>{{item.first}}</span></div>
        <div>Last name: <span>{{item.last}}</span></div>
        <button on-click="toggleSelection">Select</button>
    </template>

    <array-selector id="selector" items="{{employees}}" selected="{{selected}}" multi toggle></array-selector>

    <div> Selected employees: </div>
    <template is="dom-repeat" items="{{selected}}">
        <div>First name: <span>{{item.first}}</span></div>
        <div>Last name: <span>{{item.last}}</span></div>
    </template>
  </template>
</dom-module>

employee_list.dart:

@PolymerRegister('employee-list')
class EmployeeList extends PolymerElement {
  EmployeeList.created() : super.created();

  @property
  List<Employee> employees;

  void ready() {
    set('employees', [
      new Employee('Bob', 'Smith'),
      new Employee('Sally', 'Johnson'),
    ]);
  }

  @reflectable
  void toggleSelection(e, target) {
    // Have to use js interop to use this today, see
    // https://github.com/dart-lang/polymer_interop/issues/7
    var item = $['employeeList'].itemForElement(e.target);
    $['selector'].select(item);
  }
}

class Employee extends JsProxy {
  @reflectable
  String first;
  
  @reflectable
  String last;
  
  Employee(this.first, this.last);
}

Conditional templates

Elements can be conditionally stamped based on a boolean property by wrapping them in a custom HTMLTemplateElement type extension called dom-if. The dom-if template stamps its contents into the DOM only when its if property becomes truthy.

If the if property becomes falsy again, by default all stamped elements are hidden (but remain in the DOM tree). This provides faster performance should the if property become truthy again. To disable this behavior, set the restamp property to true. This results in slower if switching behavior as the elements are destroyed and re-stamped each time.

The following is a simple example to show how conditional templates work. Read below for guidance on recommended usage of conditional templates.

Example:

user_page.html:

<dom-module id="user-page">
  <template>
    All users will see this:
    <div>{{user.name}}</div>
    <template is="dom-if" if="{{user.isAdmin}}">
      Only admins will see this.
      <div>{{user.secretAdminStuff}}</div>
    </template>
  </template>
</dom-module>

user_page.dart:

@PolymerRegister('user-page')
class UserPage extends PolymerElement {
  UserPage.created() : super.created();
  
  @property
  User user;
}

Since it is generally much faster to hide and show elements rather than destroy and recreate them, conditional templates are only useful to save initial creation cost when the elements being stamped are relatively heavyweight and the conditional may rarely (or never) be true in given usages. Otherwise, liberal use of conditional templates can actually add significant runtime performance overhead.

Consider an app with 4 screens, plus an optional admin screen. If most users will use all 4 screens during normal use of the app, it is generally better to incur the cost of stamping those elements once at startup (where some app initialization time is expected) and simply hide/show the screens as the user navigates through the app, rather than destroy and re-create all the elements of each screen as the user navigates. Using a conditional template here may be a poor choice, since although it may save time at startup by stamping only the first screen, that saved time gets shifted to runtime latency for each user interaction, since the time to show the second screen will be slower as it must create the second screen from scratch rather than simply showing that screen. Hiding/showing elements is as simple as attribute-binding to the hidden attribute (e.g. <div hidden$="{{!shouldShow}}">), and does not require conditional templating at all.

However, using a conditional template may be appropriate in the case of an admin screen that's only shown to admin users of an app. Since most users aren't admins, there may be performance benefits to not burdening most of the users with the cost of stamping the elements for the admin page, especially if it is relatively heavyweight.

Auto-binding templates

Polymer data binding is only available in templates that are managed by Polymer. So data binding works inside an element's local DOM template, but not for elements placed in the main document.

To use Polymer bindings without defining a new custom element, use the dom-bind element. This template immediately stamps its contents into the main document. Data bindings in an auto-binding template use the template itself as the binding scope.

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <script src="components/webcomponentsjs/webcomponents-lite.js"></script>
  <link rel="import" href="components/polymer/polymer.html">
  <link rel="import" href="components/iron-ajax/iron-ajax.html">

</head>
<body>

  <!-- Wrap elements with auto-binding template to -->
  <!-- allow use of Polymer bindings in main document -->
  <template id="t" is="dom-bind">

    <iron-ajax url="http://..." last-response="{{data}}" auto></iron-ajax>

    <template is="dom-repeat" items="{{data}}">
        <div><span>{{item.first}}</span> <span>{{item.last}}</span></div>
    </template>

  </template>

</body>
<script type="application/dart">
  main() {
    var t = document.querySelector('#t');
    
    // The dom-change event signifies when the template has stamped its DOM.
    t.on['dom-change'].listen((e) {
      // auto-binding template is ready.
    });
  }
</script>
</html>

All of the features in dom-bind are already available inside a Polymer element. Auto-binding templates should only be used outside of a Polymer element.

dom-change event

When one of the template helper elements updates the DOM tree, it fires a dom-change event.

In most cases, you should interact with the created DOM by changing the model data, not by interacting directly with the created nodes. For those cases where you need to access the nodes directly, you can use the dom-change event.