Two-way Binding for Polymer Elements in Angular 2 for Dart

Boy...that title sure is a mouthful. But it's accurate!

Polymer provides many great shortcuts in the form of web components that provide us a lot of canned functionality that we don't have to recreate ourselves. Angular 2 is a fantastic (and already very popular) application framework that gives structure to even our biggest apps. Each technology comes with its own version of data binding, and sometimes this makes things unnecessarily hard when we want to combine the two.

Note: If you don't have experience combining Angular 2 and Polymer in a Dart application, do yourself a favor and read Dart, Angular 2, and Polymer Together. It'll teach you all you need to know in life.

Just Polymer

Take, for example, Polymer's awesome tabs component, paper-tabs. Combined with iron-pages, you can have a working, semantically beautiful navigation system for your web app in place in a minute or less (I'm timing you):

<paper-tabs selected="{{selected}}">
  <paper-tab>Tab 1</paper-tab>
  <paper-tab>Tab 2</paper-tab>
  <paper-tab>Tab 3</paper-tab>
</paper-tabs>

<iron-pages selected="[[selected]]">
  <div>Page 1</div>
  <div>Page 2</div>
  <div>Page 3</div>
</iron-pages>

If you place the above code into a Polymer app, it just works (after you add the proper imports and a bit of CSS for sizing, of course). The tabs and pages components are each binding to a property called selected, and this keeps them in sync. Clicking Tab 1 takes you to Page 1, and so on. The {{ }} syntax creates a two-way binding with the enclosed property on the parent component, and the [[ ]] syntax creates a one-way binding into <iron-pages> for the same property. You don't even have to explicitly declare selected on the parent component and this will work!

Bring On the Angular

But Angular 2 does binding a bit differently. A naive approach to adding this same code to an Angular app might look like this:

<paper-tabs [(selected)]="selected">
  <paper-tab>Tab 1</paper-tab>
  <paper-tab>Tab 2</paper-tab>
  <paper-tab>Tab 3</paper-tab>
</paper-tabs>

<iron-pages [selected]="selected">
  <div>Page 1</div>
  <div>Page 2</div>
  <div>Page 3</div>
</iron-pages>

As you may have guessed, this doesn't work, and without me to keep you honest, you'd probably just give up at this point. Since <paper-tabs> doesn't emit a selectedChange event, Angular has no way of knowing when the Polymer element's selected property has changed. This element fires an iron-select event when something activates a tab, and that's the key to teaching Angular how to handle it.

A Directive Can Fix It

It turns out you can use the naive approach just fine as long as you also include a little Angular directive to teach the framework a new trick.

paper_tabs_selected_dir.dart

import 'package:angular2/core.dart';
import 'package:polymer/polymer.dart';

@Directive(selector: 'paper-tabs[selected]')
class PaperTabsSelectedDirective {
  @Output() EventEmitter selectedChange = new EventEmitter();

  @HostListener('iron-select', const ['\$event'])
  void onChange(e) =>
    selectedChange.add(convertToDart(e).currentTarget.selected);
}

This directive will attach itself like a barnacle to any <paper-tabs> element that includes a selected attribute. Once there, it will start listening to iron-select events. When it hears them, PaperTabsSelectedDirective emits its own selectedChange event, attaching the value of the <paper-tabs> element's selected property as the payload. It should be noted that iron-select events are JavaScript events at heart, and we need to use convertToDart() from the Polymer package to make things kosher before trying to play with its properties.

Once you've created the directive, you need to remember to import it and include it in your Angular component's declaration:

...

import 'paper_tabs_selected_dir.dart';

@Component(selector: 'my-app',
    encapsulation: ViewEncapsulation.Native,
    templateUrl: 'app_component.html',
    directives: const [PaperTabsSelectedDirective]
)
class AppComponent {

  int selected = 0;

...

Unlike with Polymer, it is necessary to declare properties used in Angular bindings, so you'll want to include the selected property in there. Bonus: You can initialize it to something sensible when you do.

And That's It

I haven't done too much of it yet, but I suspect this same technique could be applied to any number of other alien components you want to incorporate into your Angular 2 app. The directive basically translates one event into another so the separate systems can communicate.

Happy hacking!