Communication & Message Passing
Polymer v0.3.3
Table of contents
You’re off creating new elements in HTML! There will come a time when you need to have one element send a message to another element (or set of elements). I’m using “message passing” as an overloaded term. What I really mean is relaying information between elements or to the world outside of a custom element.
In Polymer, there a slew of reasons why you might need such a communication channel:
- Element B’s internal data model changes. Element A needs to be notified.
- Element A updates its UI based on a selection in Element B.
- A third sibling, Element C, fires an event that A and B to react to.
In this article, I outline several techniques for sending information to other elements. It’s worth pointing out that most of these techniques are not specific to Polymer. They’re standard ways to make DOM elements interact with each other. The only difference is that now we’re in complete control of HTML! We implement the control hooks that users tap in to.
Methods
We’ll cover the following techniques:
1. Data binding
Pros | Limitations |
---|---|
|
|
The first (and most Polymeric) way for elements to relay information to one another is to use data binding. Polymer implements two-way data binding. Binding to a common property is useful if you’re working inside a Polymer element and want to “link” elements together via their published properties.
Here’s an example:
<!-- Publish .items so others can use it for attribute binding. -->
<polymer-element name="td-model" attributes="items">
<script>
Polymer('td-model', {
ready: function() {
this.items = [1, 2, 3];
}
});
</script>
</polymer-element>
<polymer-element name="my-app">
<template>
<td-model items="{{list}}"></td-model>
<core-localstorage name="myapplist" value="{{list}}"></core-localstorage>
</template>
<script>
Polymer('my-app', {
ready: function() {
// Initialize the instance's "list" property to empty array.
this.list = this.list || [];
}
});
</script>
</polymer-element>
When a Polymer element publishes one of its properties, you can bind to that property using an HTML attribute of the same name. In the example,
<td-model>.items
and <core-localstorage>.value
are bound together with “list”:
<td-model items="{{list}}"></td-model>
<core-localstorage name="myapplist" value="{{list}}"></core-localstorage>
What’s neat about this? Whenever <td-model>
updates its items
array internally,
elements that bind to list
on the outside see the changes. In this
example, <core-localstorage>
. You can think of “list” as an
internal bus within <my-app>
. Pop some data on it; any elements that care about
items
are magically kept in sync by data-binding. This means there is one source
of truth. Data changes are simultaneously reflected in all contexts. There is no ‘dirty check’.
Remember: Property bindings are two-way. If <core-localstorage>
changes list
, <td-model>
’s items will also change.
Property serialization
Wait a sec…“list” is an array. How can it be property bound as an HTML string attribute?
Polymer is smart enough to serialize primitive types (numbers, booleans, arrays, objects) when they’re used in attribute bindings. As seen in the ready()
callback
of <my-app>
, be sure to initialize and/or hint the type of your properties.
2. Changed watchers
Pros | Limitations |
---|---|
|
|
Let’s say someone creates <core-localstorage2>
and you want to use its new
hotness. The element still defines a .value
property but it doesn’t publish the
property in attributes=""
. Something like:
<polymer-element name="core-localstorage2" attributes="name useRaw">
<template>...</template>
<script>
Polymer('core-localstorage2', {
value: null,
valueChanged: function() {
this.save();
},
...
});
</script>
</polymer-element>
When it comes time to use this element, we’re left without a (value
) HTML attribute
to bind to:
<core-localstorage2 name="myapplist" id="storage"></core-localstorage2>
A desperate time like this calls for a changed watcher and
a sprinkle of data-binding. We can exploit the fact that <core-localstorage2>
defines
a valueChanged()
watcher. By setting up our own watcher for list
, we can
automatically persist data to localStorage
whenever list
changes!
<polymer-element name="my-app">
<template>
<td-model items="{{list}}"></td-model>
<core-localstorage2 name="myapplist" id="storage"></core-localstorage2>
</template>
<script>
Polymer('my-app', {
ready: function() {
this.list = this.list || [];
},
listChanged: function() {
this.$.storage.value = this.list;
}
});
</script>
</polymer-element>
When list
changes, the chain reaction is set in motion:
- Polymer calls
<my-app>.listChanged()
- Inside
listChanged()
,<core-localstorage2>.value
is set - This calls
<core-localstorage2>.valueChanged()
valueChanged()
callssave()
which persists data tolocalStorage
Tip: I’m using a Polymer feature called automatic node finding to reference <core-localstorage>
by its id
(e.g. this.$.storage === this.querySelector('#storage')
).
3. Custom events
Pros | Limitations |
---|---|
|
|
A third technique is to emit custom events from within your element. Other elements
can listen for said events and respond accordingly. Polymer has two nice helpers for sending events, fire() and asyncFire(). They’re essentially wrappers around node.dispatchEvent(new CustomEvent(...))
. Use the asynchronous version for when you need to fire an event after microtasks have completed.
Let’s walk through an example:
<polymer-element name="say-hello" attributes="name">
<template>Hello {{name}}!</template>
<script>
Polymer('say-hello', {
sayHi: function() {
this.fire('said-hello');
}
});
</script>
</polymer-element>
Calling sayHi()
fires an event named 'said-hello'
. And since custom events bubble,
a user of <say-hello>
can setup a handler for the event:
<say-hello name="Larry"></say-hello>
<script>
var sayHello = document.querySelector('say-hello');
sayHello.addEventListener('said-hello', function(e) {
...
});
sayHello.sayHi();
</script>
As with normal DOM events outside of Polymer, you can attach additional data to a custom event. This makes events an ideal way to distribute arbitrary information to other elements.
Example: include the name
property as part of the payload:
sayHi: function() {
this.fire('said-hello', {name: this.name});
}
And someone listening could use that information:
sayHello.addEventListener('said-hello', function(e) {
alert('Said hi to ' + e.detail.name + ' from ' + e.target.localName);
});
Using declarative event mappings
The Polymeric approach to events is to combine event bubbling
and on-*
declarative event mapping.
Combining the two gives you a declarative way to listen for events and requires very little code.
Example: Defining an on-click
that calls sayHi()
whenever the element is clicked:
<polymer-element name="say-hello" attributes="name" on-click="{{sayHi}}">
<template>Hello {{name}}!</template>
<script>
Polymer('say-hello', {
sayHi: function() {
this.fire('said-hello', {name: this.name});
}
});
</script>
</polymer-element>
Without Polymer’s sugaring
The same can be done by adding a click listener in the element’s ready
callback:
<polymer-element name="say-hello" attributes="name">
<template>Hello {{name}}!</template>
<script>
Polymer('say-hello', {
ready: function() {
this.addEventListener('click', this.sayHi);
},
sayHi: function() {...}
});
</script>
</polymer-element>
Utilizing event delegation
You can setup internal event delegation for your element by declaring an on-*
handler on the <polymer-element>
definition. Use it to catch events that bubble
up from children.
Things become very interesting when several elements need to respond to an event.
<polymer-element name="my-app" on-said-hello="{{third}}">
<template>
<div on-said-hello="{{second}}">
<say-hello name="Eric" on-said-hello="{{first}}"></say-hello>
</div>
</template>
<script>
(function() {
function logger(prefix, detail, sender) {
alert(prefix + ' Said hi to ' + detail.name +
' from ' + sender.localName);
}
Polymer('my-app', {
first: function(e, detail, sender) {
logger('first():', detail, sender);
},
second: function(e, detail, sender) {
logger('second():', detail, sender);
},
third: function(e, detail, sender) {
logger('third():', detail, sender);
}
});
})();
</script>
</polymer-element>
<div id="container">
<my-app></my-app>
</div>
<script>
var container = document.querySelector('#container');
container.addEventListener('said-hello', function(e) {
alert('outside: Said hi to ' + e.detail.name + ' from ' + e.target.localName);
});
</script>
Try it:
Clicking <say-hello>
alerts the following (remember it defined
a click handler on itself):
first(): Said hi to Eric from say-hello
second(): Said hi to Eric from div
third(): Said hi to Eric from my-app
outside: Said hi to Eric from my-app
Sending messages to siblings/children
Say you wanted an event that bubbles up from one element to also fire on sibling or child elements. That is:
<polymer-element name="my-app" on-said-hello="{{sayHi}}">
<template>
<say-hello name="Bob"></say-hello>
<say-bye></say-bye> <!-- Defines an internal listener for 'said-hello' -->
...
When <say-hello>
fires said-hello
, it bubbles and sayHi()
handles it.
However, suppose <say-bye>
has setup an internal listener for the same event.
It wants in on the action! Unfortunately, this means we can no longer exploit
the benefits of event bubbling…by itself.
This particular problem isn’t new to the web but you can easily handle it in
Polymer. Just use event delegation and manually fire the event
on <say-bye>
.
Polymer('my-app', {
sayHi: function(e, details, sender) {
// Fire 'said-hello' on all <say-bye> in the element.
[].forEach.call(this.querySelectorAll('say-bye'), function(el, i) {
el.fire('said-hello', details);
});
}
});
Using <core-signals>
<core-signals>
is a utility element that
makes the pubsub pattern a bit easier, It also works outside of Polymer
elements.
Your element fires core-signal
and names the signal in its payload:
this.fire('core-signal', {name: "new-data", data: "Foo!"});
This event bubbles up to document
where a handler constructs and dispatches
a new event, core-signal-new-data
, to all instances of <core-signals>
.
Parts of your app or other Polymer elements can declare a <core-signals>
element to catch the named signal:
<core-signals on-core-signal-new-data="{{newData}}"></core-signals>
Lowercase event names. When you use a declarative handler, the event name
is converted to lowercase, because attributes are case-insensitive.
So the attribute on-core-signal-newData
sets up a listener for core-signal-newdata
,
not core-signal-newData
. To avoid confusion, always use lowercase event names.
Here’s a full example:
<link rel="import" href="bower_components/core-signals/core-signals.html">
<polymer-element name="sender-element">
<template>Hello</template>
<script>
Polymer('sender-element', {
domReady: function() {
// name should be lowercase (with or without dashes) e.g. new-data
this.fire('core-signal', {name: "new-data", data: "Foo!"});
}
});
</script>
</polymer-element>
<polymer-element name="my-app">
<template>
<core-signals on-core-signal-new-data="{{newData}}"></core-signals>
<content></content>
</template>
<script>
Polymer('my-app', {
newData: function(e, detail, sender) {
this.innerHTML += '<br>[my-app] got a [' + detail + '] signal<br>';
}
});
</script>
</polymer-element>
<!-- Note: core-signals works outside of Polymer.
Here, sender-element is outside of a Polymer element. -->
<sender-element></sender-element>
<my-app></my-app>
4. Use an element’s API
Lastly, don’t forget you can always orchestrate elements by using their public methods (API). This may seem silly to mention but it’s not immediately obvious to most people.
Example: instruct <core-localstorage>
to save its data by call
it’s save()
method (code outside a Polymer element):
<core-localstorage name="myname" id="storage"></core-localstorage>
<script>
var storage = document.querySelector('#storage');
storage.useRaw = true;
storage.value = 'data data data!';
storage.save();
</script>
Conclusion
The unique “messaging” features that Polymer brings to the table are two-way data-binding and changed watchers. However, data binding has been a part of other frameworks for a long time, so technically it’s not a new concept.
Whether you’re inside or outside a <polymer-element>
, there are plenty of
ways to send instructions/messages/data to other web components. Hopefully,
you’re seeing that nothing has changed in the world of custom elements. That’s
the point :) It’s the same web we’ve always known…just more powerful!