Skip to content

Latest commit

 

History

History
531 lines (436 loc) · 17.7 KB

File metadata and controls

531 lines (436 loc) · 17.7 KB
layout default
title Communication & Message Passing
subtitle Custom Elements
article
author published updated polymer_version description
ebidel
2013-08-12
2013-12-17
0.0.20130808
Techniques for passing messages to, from, and inbetween custom elements.
tags
signaling
messaging
communication

{% include authorship.html %}

{% include toc.html %}

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 {{site.project_title}}, 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 {{site.project_title}}. 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
  2. Changed watchers
  3. Custom events
  4. Using an element's API

1. Data binding {#binding}

ProsLimitations
  1. No code required
  2. "Messaging" is two-way between elements
  1. Only works inside a {{site.project_title}} element
  2. Only works between {{site.project_title}} elements

The first (and most {{site.project_title}}ic) way for elements to relay information to one another is to use data binding. {{site.project_title}} implements two-way data binding. Binding to a common property is useful if you're working inside a {{site.project_title}} element and want to "link" elements together via their published properties.

Here's an example:

{% raw %} <script> Polymer('td-model', { ready: function() { this.items = [1, 2, 3]; } }); </script>

<polymer-element name="my-app">
  <template>
    <td-model items="{{list}}"></td-model>
    <polymer-localstorage name="myapplist" value="{{list}}"></polymer-localstorage>
  </template>
  <script>
    Polymer('my-app', {
      ready: function() {
        // Initialize the instance's "list" property to empty array.
        this.list = this.list || [];
      }
    });
  </script>
</polymer-element>

{% endraw %}

When a {{site.project_title}} 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 <polymer-localstorage>.value are bound together with "list":

{% raw %} {% endraw %}

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, <polymer-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 no dirty check.

Remember: Property bindings are two-way. If <polymer-localstorage> changes list, <td-model>'s items will also change. {: .alert .alert-info }

Property serialization

Wait a sec..."list" is an array. How can it be property bound as an HTML string attribute?

{{site.project_title}} 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 {#changedwatchers}

ProsLimitations
  1. Useful for elements that haven't published a property for attribute binding.
  2. Allows an element to observe its own data, regardless of how it's modified.
  1. Only works inside a {{site.project_title}} element.
  2. Requires some knowledge of an element's API
  3. Small amount of code hookup

Let's say someone creates <polymer-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="polymer-localstorage2" attributes="name useRaw">
<template>...</template>
<script>
  Polymer('polymer-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:

<polymer-localstorage2 name="myapplist" id="storage"></polymer-localstorage2>

A desperate time like this calls for a changed watcher and a sprinkle of data-binding. We can exploit the fact that <polymer-localstorage2> defines a valueChanged() watcher. By setting up our own watcher for list, we can automatically persist data to localStorage whenever list changes!

{% raw %} <script> Polymer('my-app', { ready: function() { this.list = this.list || []; }, listChanged: function() { this.$.storage.value = this.list; } }); </script> {% endraw %}

When list changes, the chain reaction is set in motion:

  1. {{site.project_title}} calls <my-app>.listChanged()
  2. Inside listChanged(), <polymer-localstorage2>.value is set
  3. This calls <polymer-localstorage2>.valueChanged()
  4. valueChanged() calls save() which persists data to localStorage

Tip: I'm using a {{site.project_title}} feature called automatic node finding to reference <polymer-localstorage> by its id (e.g. this.$.storage === this.querySelector('#storage')). {: .alert .alert-success }

3. Custom events {#events}

ProsLimitations
  1. Works with elements inside and outside a {{site.project_title}} element
  2. Easy way to pass arbitrary data to other elements
  3. Inside a {{site.project_title}} element, using on-* declarative handlers reduces code
  4. Can use bubbling for internal event delegation in an element.
  1. Small amount of code hookup
  2. Requires some knowledge of an element's API

A third technique is to emit custom events from within your element. Other elements can listen for said events and respond accordingly. {{site.project_title}} 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:

{% raw %} Hello {{name}}! <script> Polymer('say-hello', { sayHi: function() { this.fire('said-hello'); } }); </script> {% endraw %}

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 {{site.project_title}}, 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 {#declartivemappings}

The {{site.project_title}}ic approach to events is 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:

{% raw %} Hello {{name}}! <script> Polymer('say-hello', { sayHi: function() { this.fire('said-hello', {name: this.name}); } }); </script> {% endraw %}

Without {{site.project_title}}'s sugaring

The same can be done by adding a click listener in the element's ready callback:

{% raw %} Hello {{name}}! <script> Polymer('say-hello', { ready: function() { this.addEventListener('click', this.sayHi); }, sayHi: function() {...} }); </script> {% endraw %}

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 come very interesting when several elements need to respond to an event.

{% raw %}

<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>

<my-app></my-app>

<script>
  var myApp = document.querySelector('my-app');
  myApp.addEventListener('said-hello', function(e) {
    alert('outside: Said hi to ' + e.detail.name + ' from ' + e.target.localName);
  });
</script>

{% endraw %}

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
outside: Said hi to Eric from my-app
third(): 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:

{% raw %} ... {% endraw %}

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 {{site.project_title}}. 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 <polymer-signals>

<polymer-signals>is a utility element that makes the pubsub pattern a bit easier, It also works outside of {{site.projed}} elements.

Your element fires polymer-signal and names the signal in its payload:

this.fire('polymer-signal', {name: "foo", data: "Foo!"});

This event bubbles up to document where a handler constructs and dispatches a new event, polymer-signal-foo, to all instances of <polymer-signals>. Parts of your app or other {{site.project_title}} elements can declare a <polymer-signals> element to catch the named signal:

{%raw%} {%endraw%}

Here's a full example:

<polymer-element name="sender-element">
  <template>Hello</template>
  <script>
    Polymer('sender-element', {
      ready: function() {
        this.asyncFire('polymer-signal', {name: "foo", data: "Foo!"});
      }
    });
  </script>
</polymer-element>

<link rel="import" href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fpolymer-signals.html">

<polymer-element name="my-app">
  <template>
    <polymer-signals on-polymer-signal-foo="{%raw%}{{fooSignal}}{%endraw%}"></polymer-signals>
    <content></content>
  </template>
  <script>
    Polymer('my-app', {
      fooSignal: function(e, detail, sender) {
        this.innerHTML += '<br>[my-app] got a [' + detail + '] signal<br>';
      }
    });
  </script>
</polymer-element>

<!-- Note: polymer-signals works outside of {{site.project_title}}.
     Here, sender-element is outside of a {{site.project_title}} element. -->
<sender-element></sender-element>
<my-app></my-app>

4. Use an element's API {#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 <polymer-localstorage> to save its data by call it's save() method (code outside a {{site.project_title}} element):

<polymer-localstorage name="myname" id="storage"></polymer-localstorage>
<script>
  var storage = document.querySelector('#storage');
  storage.useRaw = true;
  storage.value = 'data data data!';
  storage.save();
</script>

Conclusion

The unique "messaging" feature that {{site.project_title}} brings to the table 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!

<script> var myApp = document.querySelector('my-app-demo'); myApp.addEventListener('said-hello', function(e) { alert('outside: Said hi to ' + e.detail.name + ' from ' + e.target.localName); }); </script>

{% include disqus.html %}