Tuesday, August 13, 2013

Translating the Web-UI x-click-counter to Polymer.dart

This blog post shows how to take the Web UI sample "x-click-component" project created from Dart Editor wizard, and convert it into a Polymer.dart project.  Along the way, I'll highlight some of the differences between Web-UI and Polymer (and some of the similarities).

Dart's web components project, called Web-UI, is migrating over to the Polymer project's web component framework.

One of the key differences between the Dart's Web UI project and Polymer is that Polymer doesn't require a separate build step (or the /out/ folder), at least while you're developing (you'll still want a build step when you deploy, to combine all the external imports).

Note: This blog post was written using Dart 0.6.15 r25822 and Polymer 0.5.3.

Creating the project


Let's start by creating a new project in the editor.

Create a new Web Application (using the web_ui library) project
This creates you a web project structure containing a set of example files.  After a few seconds, pub has installed the various packages and build.dart has finished running, your folder structure probably looks something like this:
The starting web-ui folder structure

Removing Web UI and Build.dart

The first step (and I'm sure you'll do this with a smile on your face) is to stop build.dart running.  Right click it, and click the handy menu option "Don't run build.dart"

Next, open the pubspec.yaml file, and remove all the current dependencies (likely to be browser, js and web_ui).  Add a polymer dependency:

Changing the custom element to Polymer

In your project there will be two files that represent the existing Web UI x-click-counter custom element.  These are xclickcounter.html, which provides the layout, and xclickcounter.dart, which provides the script.  

The existing x-click-counter defines itself as a custom element, has a button that lets you click it to increment a data bound value.  It looks like the screenshot below:
Example screenshot of the x-click-counter component
We'll go through a couple of iterations to modify this to Polymer.  First, we'll just get the custom element to show up in the page, and then we'll come back and look at the interactivity and data binding.

Before modification, xclickcounter.html looks like this:

xclickcounter.html before modification:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>

<html>
  <body>
    <element name="x-click-counter" constructor="CounterComponent" extends="div">
      <template>
        <div>
          <button on-click="increment()">Click me</button><br />
          <span>(click count: {{count}})</span>
        </div>
      </template>
      <script type="application/dart" src="xclickcounter.dart"></script>
    </element>
  </body>
</html>

There are three key parts that you need to be aware of in the original Web UI element:

  • Line 5: The <element> tag
  • Line 8: Calling an increment() function
  • Line 9: Databinding a {{count}} value
Start by modifying this to be a polymer element.  Change the <element> tag on line 5 to be a <polymer-element> and update the closing </element> tag on line 13. A polymer element is a like a supercharged custom element, with functionality provided by the Polymer framework.  It automatically uses things like the Shadow DOM and allows for simple component registration.
Notice also that the name is changed from x-click-counter to my-clickcounter, (for the sake of simplicity, I'm not going to rename the filenames).  I've also removed the {{count}} binding for now. We'll add it back again, soon

xclickcounter.html after modification:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>

<html>
  <body>
    <polymer-element name="my-clickcounter">
      <template>
        <div>
          <button on-click="increment()">Click me</button><br />
          <span>(click count: )</span>
        </div>
      </template>
      <script type="application/dart" src="xclickcounter.dart"></script>
    </polymer-element>
  </body>
</html>

Important:  Web Component custom elements names need to contain a hyphen. The intention is that the first part before the hyphen is a vendor prefix (to differentiate between different clickcounter components, for example).  See this discussion on the polymer mailing lists.
Let's change the associated script in the xclickcounter.dart file.  This represents our element in code.  Before modification it looks like this:

xclickcounter.dart before modification:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import 'package:web_ui/web_ui.dart';

class CounterComponent extends WebComponent {
  @observable
  int count = 0;

  void increment() {
    count++;
  }
}
First, let's change line 1 to import the polymer libary:


import 'package:polymer/polymer.dart';


Then let's change line 3 - CounterComponent class to extend PolymerElement instead of WebComponent


class CounterComponent extends PolymerElement {

Finally, we need to provide a method of registering our my-clickcounter element with the browser. The JavaScript version of Polymer elements do this using a JavaScript call, but in Dart, we can use a @CustomTag('my-clickcounter') class annotation.  The full changed xclickcounter.dart file is shown below (note, we'll return to the commented out functionality in a few steps:

xclickcounter.dart after modification:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import 'package:polymer/polymer.dart';

@CustomTag("my-clickcounter")
class CounterComponent extends PolymerElement {
//  @observable
//  int count = 0;
//
//  void increment() {
//    count++;
//  }
}

Now that the layout and script for the custom element is changed to use Polymer, let's see how you actually use them in your application's main page.

Using the Custom Element in your page

Currently, our main page (called polymerclicker1.html), and our main script (called polymerclicker1.dart) use Web UI.  The Web UI version of the files look like this

polymerclicker1.dart before modification:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import 'dart:html';
import 'package:web_ui/web_ui.dart';

// initial value for click-counter
int startingCount = 5;

/**
 * Learn about the Web UI package by visiting
 * http://www.dartlang.org/articles/dart-web-components/.
 */
void main() {
  // Enable this to use Shadow DOM in the browser.
  //useShadowDom = true;
}

polymerclicker1.html before modification


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>

<html>
  <head>
    <meta charset="utf-8">
    <title>Sample app</title>
    <link rel="stylesheet" href="polymerclicker1.css">
    
    <!-- import the click-counter -->
    <link rel="import" href="xclickcounter.html">
  </head>
  <body>
    <h1>Polymerclicker1</h1>
    
    <p>Hello world from Dart!</p>
    
    <div id="sample_container_id">
      <div is="x-click-counter" id="click_counter" count="{{startingCount}}"></div>
    </div>

    <script type="application/dart" src="polymerclicker1.dart"></script>
    <script src="packages/browser/dart.js"></script>
  </body>
</html>

It only takes a couple of simple modifications to change these to use Polymer.   To start with, you can remove everything from the .dart file apart from an empty main() function.  We'll add something back into this once we start data binding a little later.

polymerclicker1.dart after modification:

1
2
3
void main() {
  // no content here yet.
}

Next, lets modify the HTML file.  This introduces a new piece of functionality, in the form of boot.js, found in the Polymer package.  The comments from that file explain fairly succinctly (we'll see it in action in a moment).

This script dynamically prepares a set of files to run polymer.dart. It usesthe html_import polyfill to search for all imported files, then it inlines all <polymer-element> definitions on the top-level page (needed by registerPolymerElement), and it removes script tags that appear inside those tags. It finally rewrites the main entrypoint to call an initialization function on each of the declared <polymer-elements>.
To get this working, replace the existing JavaScript script import on Line 22:  packages/browser/dart.js with polymer's packages/polymer/boot.js.

Let's also modify the #click_counter div to be an actual my-clickcounter element, by changing line 18 so that...


<div is="x-click-counter" id="click_counter" count="{{startingCount}}"></div>
  <!-- becomes... -->
<my-clickcounter id="click_counter" count="{{startingCount}}">
                                                           </my-clickcounter>

Now, the polymerclicker1.html should look like this, with lines 18 and 22 changed.:

polymerclicker1.html after modification


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>

<html>
  <head>
    <meta charset="utf-8">
    <title>Sample app</title>
    <link rel="stylesheet" href="polymerclicker1.css">
    
    <!-- import the click-counter -->
    <link rel="import" href="xclickcounter.html">
  </head>
  <body>
    <h1>Polymerclicker1</h1>
    
    <p>Hello world from Dart!</p>
    
    <div id="sample_container_id">
      <my-clickcounter id="click_counter" count="{{startingCount}}"></my-clickcounter>      
    </div>

    <script type="application/dart" src="polymerclicker1.dart"></script>
    <script src="packages/polymer/boot.js"></script>
  </body>
</html>

Running in Dartium

You can go ahead and run this in now Dartium.  It doesn't have any interactivity (it will throw an error when you click the button), but it's worked without the build step
Tip: you might need to delete the web/out/ subfolder created by the old build.dart if you're being automatically redirected.

Let's take a look under the covers and see what's happened.  The old build.dart used to examine the static HTML page and combine all the HTML Imports into a runnable set of files before the application was run.  The polymer boot.js does this, but at runtime by replacing the our applications main() function (currently empty in polymerclicker1.dart) with a higher level main function that imports all the web componetns, initializes polymer and finally calls our own main() function (identified as userMain.main).  If you inspect elements in Dartium, you'll see the new script looks something like this:


1
2
3
4
5
6
7
8
9
import "http://127.0.0.1:3030/web/xclickcounter.dart" as i0;
import "package:polymer/polymer.dart" as polymer;
import "http://127.0.0.1:3030/web/polymerclicker1.dart" as userMain;

main() {
  polymer.initPolymer([
      "http://127.0.0.1:3030/web/xclickcounter.dart"
      ], userMain.main);
}

This new main() function passes in a list of all the polymer elements discovered through HTML Imports, and a callback to the original userMain.main() function.

From comments like this in the boot.js,
//TODO: rephrase when we split build.dart in two: analysis vs deploy pieces.
console.warn('boot.js only works in Dartium. Run the build.dart  tool to compile a depolyable JavaScript version')
it seems that the intention is that this is only used for development.

Adding data binding to the custom element

At the moment we're using the custom element, but it has no interactivity or data binding.  Remember that {{count}} we removed?  Let's put it back.  Polymer.dart uses an ObservableMixin to add the ability to observe changes to a variable through the @observable annotation.  This annotation allows two way data binding between the count field in the class instance, and any {{ count }} declarations in the template.
Uncomment the previously commented out section (lines 6 through 11), and add the ObservableMixin to the class definition (line 5) in xclickcounter.dart.

xclickcounter.dart after modification with functionality and observable mixin


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import 'package:polymer/polymer.dart';

@CustomTag("my-clickcounter")
class CounterComponent extends PolymerElement 
                             with ObservableMixin {
  @observable
  int count = 0;

  void increment() {
    count++;
  }
}

Let's also put the {{count}} binding value back into the xclickcounter.html at line 9.  The on-click="increment()" function call on line 8 changes as well - removing the parentheses (this is inline with the JavaScript version of Polymer).

xclickcounter.html after modification with functionality and data binding.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>

<html>
  <body>
    <polymer-element name="my-clickcounter">
      <template>
        <div>
          <button on-click="increment">Click me</button><br />
          <span>(click count: {{ count }})</span>
        </div>
      </template>
      <script type="application/dart" src="xclickcounter.dart"></script>
    </polymer-element>
  </body>
</html>

Finally, run the same polymerclicker1.html application, and you'll see that you can now click the "Click Me" button to increment the count.


Binding initial data values

The last step we need to take is to databind an initial value.  The original Web UI click counter initialized the counter with a value of startingCount=5. I've not found a way to do this exactly yet (and it may be that this part of Polymer hasn't been ported over yet), but you can achieve it through template binding.

The first step is to publish a public attribute on the my-clickcounter polymer element.  You do this using the attributes="" attribute on the <polymer-element> definition, making public any properties in the underlying Dart class.  Change Line 5 of xclickcounter.html as shown below:

xclickcounter.html after modification with published count attribute


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>

<html>
  <body>
    <polymer-element name="my-clickcounter" attributes="count">
      <template>
        <div>
          <button on-click="increment">Click me</button><br />
          <span>(click count: {{ count }})</span>
        </div>
      </template>
      <script type="application/dart" src="xclickcounter.dart"></script>
    </polymer-element>
  </body>
</html>

Now that the count property is published, let's bind a value to it in our main polymerclicker1 page.  To do this, you need to wrap the my-clickcounter in a template binding and bind that template to a model object, accessible through the elements model property in Dart code.

The template wrapping looks like this, in the polymerclicker1.html file:

<template id="counter_template" bind>
  <my-clickcounter id="click_counter" count="{{}}"></my-clickcounter>
</template>

Template binding uses the {{}} notation to bind to the model's value (rather than a property of the model).
We also need to modify the previously empty polymerclicker1.dart file to set the template model's value to 5.

polymerclicker1.dart after modification, setting a template model value


1
2
3
4
5
import 'dart:html';

void main() {
  query("#counter_template").model = 5;
}

Now, when you run this with the changes, the counter starts with a value of 5, and increments in response to button clicks.

Summary

In this example, you saw how to import custom Polymer elements without requiring a build step by using the new boot.js file imported from the polymer package.  This dynamically imports the various elements at runtime, rather than requiring a static build.
You also saw some simple data binding within an element itself using the @observable annotation (unchanged from Web UI).
Finally, you saw how one way to bind data into a custom element by wrapping it in a template.

As ever, comments are welcome, especially if it's to correct something in this post.

References

Most of the documentation on Polymer currently exists on the www.Polymer-Project.org homepage, or in as comments in the various source code files in Dart's Web_UI's polymer branch.  For a more fully featured Polymer example, take a look at the TodoMVC project in the polymer branch (hint, start with index.html).

The Polymer-dev group and the Dart Web-UI group also provide a discussion forum and other information.

Get the Gist:

You can find the final versions of the files here:


5 comments:

  1. nice to see polymer getting some love. will there be a library of mixins in dart?

    ReplyDelete
  2. Very nice and easy to follow. Thanks!
    A couple of typos: (1) "the hyphen is a a vendor prefix", (2) "to make a publish a public attribute"

    ReplyDelete
  3. I've tried to change some of the CSS properties for '#click_counter' and '#click_counter button' (e.g. increasing margin-bottom) in polymerclicker1.css, but I cannot see any effect of my changes. Is there something wrong with the selectors?

    ReplyDelete
  4. Correction: The '#click_counter' selector seems to work, but not the '#click_counter button'. I ended up putting style="margin-bottom: 20px" into the button tag in xclickcounter.html.

    ReplyDelete
  5. Tried your example and it all worked.
    But when it comes to deploy it got a bit confusing (maybe because most of this is work in progress) but this is what I did.

    1 : edited build.dart to contain the following :
    library build;
    import 'package:polymer/component_build.dart';

    import 'dart:io';
    //import 'package:web_ui/component_build.dart';

    // Ref: http://www.dartlang.org/articles/dart-web-components/tools.html
    main() {
    var args = new Options().arguments.toList()..addAll(['--', '--deploy']);
    build(args,['web/polymertest.html','web/xclickcounter.html']);
    //build(new Options().arguments, ['web/polymertest.html']);
    }

    2: Then I right click on build.dart' and selected 'Run'. This produced web/out folder with some content in it.

    3: Then went back to 'Pubspec Details' and selected 'Run pub deploy'; which created a deploy folder

    4: Then I did the following
    >cd deploy/out
    >mkdir packages/shadow_dom
    >mkdir packages/browser
    >cp ../../packages/shadow_dom/shadow_dom.debug.js packages/shadow_dom/.
    >cp ../../packages/browser/interop.js packages/browser/.
    >cp ../../packages/browser/dart.js packages/browser/.

    After all that I was able to run load in chrome deploy/out/polymertest.html

    Is that rights?

    ReplyDelete