Polymer Team profile pic

Introducing Polymer 1.2.0

Polymer 1.2 introduces some exciting, long-awaited new features: compound bindings and observing light DOM children.

We announce significant releases here on the blog, highlighting the major new features as well as breaking changes. As always, we'd love to get your feedback, issues, and PR's, so tweet us @polymerLibrary or contribute to the Github.

The 1.2.0 release includes two significant new features:

  • Compound bindings. At long last, string interpolation in bindings.

  • Effective children and node observers. New features for querying and tracking changes to your light DOM children.

Compound bindings

This release delivers one of the most-asked-for features in Polymer data binding: string interpolation, or "compound bindings." You can now embed multiple binding expressions in a single string:

<div>first name: [[name.first]] last name: [[name.last]]</div>

Compound bindings can eliminate the need for computed bindings in many cases.

Note that you can use either one-way ( [[]] ) or automatic ( {{}} ) binding annotations in a compound binding, but the bindings are always one-way, host-to-target.

Distributed nodes, effective children

Often, a custom element needs to know about its light DOM children. This release includes two new APIs related to children:

  • Effective children. A new set of APIs provide a way to access an element's "effective children" — the child list with any <content> elements replaced by their distributed children.
  • observeNodes. A new method that lets you register for notification when an element's effective children or distributed children change.

To understand these new APIs, you first need to understand distributed children.

The Polymer DOM API supports the getDistributedNodes method, which returns a list of child nodes distributed to a given insertion point (<content> element).

But what about custom elements with no local DOM? or if you want to find light DOM children that haven't been distributed to any insertion point? That's when you need the effective children APIs.

Consider a simple image carousel element with no local DOM. It's used like this:

<simple-carousel>
  <img src="one.jpg">
  <img src="two.jpg">
  <img src="three.jpg">
<simple-carousel>

The carousel adds dots underneath the current image that let the user select a different image, so the carousel needs to know how many children it has. This is simple enough: the carousel can check its children the attached method:

attached: function() {
  this.childcount = Polymer.dom(this).children.length;
  // do something with childcount ...
}

But there are a few issues here. What if you create a new element, <popup-carousel>, that includes a simple carousel in its local DOM? You use the new element the same way:

<popup-carousel>
  <img src="one.jpg">
  <img src="two.jpg">
</popup-carousel>

Internally, the popup-carousel does something like this:

<dom-module id="popup-carousel">
  <template>
    <simple-carousel>
      <content></content>
    </simple-carousel>
  </template>
  ...
</dom-module>

The popup carousel simply passes its children on to the simple carousel by including a <content> tag. But now the simple carousel's attached method doesn't work: Polymer.dom(this).children.length will always return 1, because the carousel only has a single child, the <content> tag.

Clearly, children isn't what you want here. you want a list of children, with any <content> tags replaced by their distributed children. Unfortunately, the platform doesn't have a primitive for this, so Polymer has added the concept of "effective children" in its DOM API.

You can now retrieve an element's effective child nodes using:

var effectiveChildren = Polymer.dom(element).getEffectiveChildNodes();

For convenience, several new methods are available on the Polymer element prototype:

  • getEffeciveChildNodes(). Returns a list of effective child nodes for this element.
  • getEffectiveChildren(). Returns a list of effective child elements for this element.
  • queryEffectiveChildren(selector). Returns the first effective child that matches selector.
  • queryAllEffectiveChildren(selector). Returns a list of effective children that match selector.

Replacing children with the getEffectiveChildren method gives you the result you want:

this.childcount = this.getEffectiveChildren().length;

You can think of getEffectiveChildren as a composition-friendly version of children.

Effective children versus distributed children

For the simple case, if your element has a single <content> tag with no select attribute, calling getEffectiveChildNodes on your element gives the same results as getDistributedNodes on the content element. All effective child nodes are distributed to the content element:

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

  <template>
    <content id="mycontent"></content>
  </template>
  <script>
    Polymer({
      is: 'simple-content',
      ready: {
        // these two calls return the same information
        var effective = this.getEffectiveChildNodes();
        var distributed = Polymer.dom(this.$.mycontent).getDistributedNodes();
      }
    });
  </script>
</dom-module>

But what about runtime DOM changes?

Of course, there's another problem with checking the child list in ready or attached, what happens if someone adds adds or removes a child at runtime? Your element should handle that, too.

The observeNodes method provides a local-DOM-aware method to monitor DOM changes.

this._observer = Polymer.dom(node).observeNodes(function(info) {
  this.processNewNodes(info.addedNodes);
  this.processRemovedNodes(info.removedNodes);
});

observeNodes takes a callback that takes an info object. The info object includes addedNodes and removedNodes arrays. Unlike a mutation observer, the observeNodes callback is only invoked for added and removed nodes, not for other changes.

The method returns a handle that can be used to stop observation:

    Polymer.dom(node).unobserveNodes(this._observer);

The observeNodes method behaves slightly differently depending on the node being observed:

  • If the node being observed is a content node, the callback is called when the content node's distributed children change.
  • For any other node, the callback is called when the node's effective children change.

A few notes on observeNodes:

  • Since the method is attached to the DOM API, the callback is called the observed node as the this value. so if you do:
this._observer = Polymer.dom(this.$.content).observeNodes(_childrenchanged);

The callback is invoked with this.$.content as the this value. If you want to use the custom element as the this value, you need to bind the callback:

var boundHandler = this._childNodesChanged.bind(this);
this._observer = Polymer.dom(this.$.content).observeNodes(boundHandler);
  • Like a mutation observer, the callback argument lists added and removed nodes, not just elements. If you're only interested in elements, you can filter the node list:
info.addedNodes.filter(function(node) {
  return (node.nodeType === node.ELEMENT_NODE)
});
  • The first callback from observeNodes contains all nodes added to the element, not the elements added since observeNodes was called. this works well if you're using observeNodes exclusively.

    If you need to synchronously process the element's children -- for example, in attached, and then use observeNodes to monitor changes to the child list, you may need to be aware of this.

Why not just a mutation observer?

If you're familiar with mutation observers, you may wonder why you can't just use a mutation observer to handle DOM changes.

For the simple case, you can use a mutation observer to detect when children are added or removed from your element. However, mutation observers have the same limitation as the children list: they don't reflect local DOM distributions. In the case of the <popup-carousel> example, adding a child to <popup-carousel> wouldn't trigger a mutation observer on <simple-carousel>.

To detect those changes, <simple-carousel> would have to check its child list for <content> nodes. If it's got a <content> node in its children, it would need to add another mutation observer on its shadow host (in this case, <popup-carousel>). And so on. Suddenly, the <simple-carousel> isn't so simple anymore.

The observeNodes method handles this complexity for you. It uses mutation observers internally to track DOM changes, and handles the extra bookkeeping required to track local DOM distributions. Unlike a mutation observer, the observeNodes callback is only invoked when nodes are added or removed — it doesn't handle attribute changes or character data changes.

As always, we'd love to get your feedback on this release, so please tweet @polymerLibrary or check out the Polymer library on Github.