Polymer Dart Code Lab: Your First Elements

Dart! Polymer! Web components! Buzz words! Learn the facts behind the hype. Can you really build new HTML elements? Why would you want to? How is it done? In this code lab, we'll briefly go over the background of these new technologies and build a simple web app to gently introduce some of the key concepts.

Note: This code lab's full code is available on Github for those who would like to play with it or use it as a reference.

The code lab was tested with Dart SDK v1.16.0.

Credit

This Polymer Dart code lab was adapted from a similar lab for JavaScript, which was presented at Polymer Summit 2015.

Dart

Dart is an open-source, scalable, object-oriented programming language, with robust libraries and runtimes, for building web, server, and mobile apps. It was originally developed by Google, but has since become an open-source ECMA standard.

Setting Up

Briefly, this is the software you'll need to complete this code lab:

For a detailed Dart environment-setup walkthrough using WebStorm, take a look at the beginning sections of Writing Command-Line Utilities with Dart. For a similar treatment of Sublime Text 3, check out Easy Custom Web Servers with Dart and Redstone.

Note: Because you need to import custom packages that aren't part of Dart's core libraries, you will not be able to use DartPad for this project.

Dart Primers

Check out the Dart Language Tour for a crash course in the Dart language. If you already know JavaScript, Java, PHP, ActionScript, C/C++/C#, or another "curly brace" language, you'll find Dart to be familiar, and you can be productive with Dart within an hour or so.

If you like learning from videos, take a look at Learn Dart in One Video.

Polymer

From Wikipedia:

Web Components are a set of standards currently being produced by Google engineers as a W3C specification that allow for the creation of reusable widgets or components in web documents and web applications. The intention behind them is to bring component-based software engineering to the World Wide Web.

The HTML standard gives us <div> and <p> elements, and those are great for creating generic, rectangular divisions or paragraphs in web documents, but if you were building a chat app, wouldn't it be great to have something that looked this this?

  <chat-window>
    <chat-message user="Josh">Hi!</chat-message>
    <chat-message user="Jimmy">I'm here.</chat-message>
    <chat-message user="Cam">Me too.</chat-message>
    <chat-message user="Josh">I'm going to be late.</chat-message>
  </chat-window>

Simple, clean, semantic markup that communicates its purpose at a glance. A mess of nested <div> and <p> elements would be much harder to read, reason about, and modify. With native support for web component technologies in most modern browsers, and polyfills for the not-so-modern browsers, now you can create the elements that make sense for your application.

Polymer is a library that makes it easier and faster to build web components. It provides syntactic sugar to hide from you all the "plumbing" code, so you can concentrate on your app. Polymer also gives you data binding, which saves you from having to do much manual DOM manipulation. The library is originally a JavaScript project, but we will be making use of Polymer Dart in this lab.

The Polymer team has also graciously created several collections of pre-built components for your use, and these are available to you whether you use PolymerJS or Polymer Dart.

Code Lab: Your First Elements

In this code lab, you will produce a brand new HTML element: <icon-toggle>, a simple component to display an icon that can be toggled on and off in response to user input.

icon-toggle

Step 1: Gotta Start Somewhere

You should already have installed the Dart SDK from the Setting Up section above, and once that's done, you should use WebStorm or Stagehand to create your project. WebStorm uses Stagehand behind the scenes, so for either tool, your project type options will be the same. We will go over each approach to project creation below.

Create Your Project With WebStorm

If you're using WebStorm, congratulations on an excellent choice! This part is going to be easy.

  1. Do one of the following:
    • On the Welcome screen, click the link Create New Project.
    • On the main menu, choose File | New Project.
  2. In the dialog box that opens, specify the project location. Then from the left column, choose Dart as its type.
  3. Make sure the Dart SDK path and Dartium path options are set correctly, pointing to wherever you downloaded those files on your system. WebStorm should display the Version it has found.
  4. Generate sample content should be checked.
  5. From the list of Stagehand project types at the bottom, choose Polymer Web Application.

Create Your Project With Stagehand

You can use Stagehand on the command line to create Dart projects. Follow the instructions for installation on the Stagehand site (including adding the Dart SDK and Pub cache directories to your system path).

Once that's complete, and following any necessary restarts to update your path, create a directory for your Dart project. Stagehand will use the directory name as your app's name. From within the project directory, execute:

stagehand web-polymer

You'll also need to manually acquire dependencies using this command:

pub get

Step 2: Index the HTML

Open your project's index.html, which should be in the web directory.

Look at This Body

For a Polymer code lab, it's the <body> of this HTML file that's most interesting. What is a <main-app>? According to the core philosophy of Polymer, everything is an element. Or at least it should be. That includes your app itself. Stagehand has conveniently defined this element for you already.

Run It!

Your project's index.html file is the entry point for your app. Using your favorite method, load that file into Dartium to see what the sample code does for you. In WebStorm, you can right click the file in the Project Pane and select Run.

Bug! The latest version of the Dart analyzer at this writing (0.27.3) has a problem running the Polymer code transformers. To fix this, add a new dependency to your pubspec.yaml file: analyzer: 0.27.2. Don't forget to update your dependencies! With WebStorm, you can right-click pubspec.yaml and choose Pub: Get Dependencies, or on the command line you can type pub get.

You should see an input box styled with Google's Material Design guidelines. If you type something in there, the app will reverse the text, displaying the results in real time using Polymer's data binding feature.

Step 3: Main Dash App

So what's with all the dashes in custom element names, anyway? The Custom Elements specification demands it. The dash effectively allows the HTML parser to tell the difference between true custom elements and regular elements.

In keeping with good Dart package structure, you'll find the code for <main-app> in your project's lib directory. Polymer Dart custom elements are typically comprised of an HTML file containing the declarative (tag-based) code and a Dart file containing the imperative (logic) code.

We're going to take a quick look at the custom element Stagehand provided, <main-app>. Don't worry if the concepts don't stick during this whirlwind tour. You'll get a chance to dig deeper in Step 4, when you make your own element.

Main App HTML

Take a look at main_app.html. It's a great example of a very simple custom element. There are three main parts:

<dom-module> Polymer elements are defined within this tag. The id is the name of the custom element.

<template> The first <template> tag in a <dom-module> defines the body of your custom element. Typically, markup in here is added to the custom element's local DOM.

<style> This one should be familiar. This is where you put CSS that will apply to the elements within your custom element, or to the custom element itself using the :host pseudo-class. Styles defined here are scoped to the local DOM, so they don't affect the rest of the document.

Local DOM

The local DOM of <main-app> contains a <paper-input> element from Polymer's paper elements collection, as well as the more familiar <p> and <span> tags.

Data Binding

Wherever you see the curly-bracket syntax ({{ }}) in your HTML, that's an example of data binding. The input's value, when changed by the user typing something, automatically updates a property called text in main_app.dart. When text is updated, its value is automatically inserted into the <span> tags in its normal form and reversed, respectively.

Main App Dart

The main_app.dart file contains the Dart class that will be associated with the <main-app> element. This association is made by means of the @HtmlImport annotation at the top of the file. You can read more about this file's code structure in the Polymer Dart docs, but we'll examine a few features briefly here.

Property

The text property is defined in the class and annotated with @property, which makes it available as part of the element's public API, configurable from markup. Note that it's also typed as a String using Dart's optional type annotations.

Reflectable

The class's reverseText() method is annotated as @reflectable, which in this case makes it available to be used in a computed binding from the element's <template>.

Step 4: Creating a Toggling Icon

For your first from-scratch custom element, you'll create a tag that displays an icon from the Polymer iron-icons set. The icon will respond to clicks or taps by toggling on and off.

Step 4.1: Create icon_toggle.html

In your project's lib folder, create a new HTML file: icon_toggle.html. Remove any boilerplate code your development environment may have generated and add the following:

lib/icon_toggle.html

<dom-module id="icon-toggle">
  <template>
    <style>
      :host {
        display: inline-block;
      }
    </style>

  </template>
</dom-module>

Very little happening so far, but you should recognize the basic structure of a Polymer custom element from your examination of <main-app>.

Step 4.2: Create icon_toggle.dart

Also in your lib directory, create a new Dart file: icon_toggle.dart with this code:

lib/icon_toggle.dart

@HtmlImport('icon_toggle.html')
library pdcl_first_elements.lib.icon_toggle;

import 'dart:html';
import 'package:polymer/polymer.dart';
import 'package:web_components/web_components.dart' show HtmlImport;

@PolymerRegister('icon-toggle')
class IconToggle extends PolymerElement {

  IconToggle.created() : super.created();

  void ready() {
    print("$runtimeType::ready()");
  }
}
Main Points
  • The @HtmlImport annotation is used to load up icon_toggle.html.
  • You define a Dart library in this file, which is the privacy boundary in Dart. Everything in this file is part of the library.
  • You import a few other libraries to get access to code that you'll need here, including Dart's HTML library and several for Polymer. Using show HtmlImport, you bring in only that piece of code from the web_components package, rather than the whole thing.
  • @PolymerRegister tells the system which custom element your class should be associated with. The argument here should match the value of id from the element's <dom-module>. For more, see Registration and Lifecycle.
  • The class IconToggle extends the PolymerElement class, causing IconToggle to inherit all the functionality of a Polymer element. Check out the docs for Dart classes for more on how object-oriented programming in Dart works.
  • All Polymer Dart custom element classes must have a named constructor called created(), which executes the superclass's constructor of the same name.
  • The ready() method is part of a Polymer element's lifecycle. If present, it is called when your element's local DOM is fully initialized and ready to rock. Here, you use Dart's string interpolation feature and the print() function to output a quick message to the browser console, allowing you to verify that your custom element has been instantiated.

Step 4.3: Use Your New Element

Before you can use your new <icon-toggle> element, you need to import it. Add this import statement with the others at the top of the main app's Dart file:

lib/main_app.dart

import 'icon_toggle.dart';

Now you can add a couple of instances of <icon-toggle> to the view. Replace the existing visual elements in the <template> with these:

lib/main_app.html

<template>
  <style>
  ...
  </style>

  <h3>Statically-configured icon-toggles</h3>
  <icon-toggle></icon-toggle>
  <icon-toggle pressed></icon-toggle>
</template>

The pressed Boolean attribute doesn't do anything yet, but you'll make use of it soon enough.

If you run the app now, only the contents of the <h3> will show up since <icon-toggle> has an empty <template>, but you should see evidence that both of them are being attached to the DOM in the browser console:

console output

IconToggle::ready()
IconToggle::ready()

Note that Chrome/Dartium's console may only print the output once, but there should be a "2" nearby to indicate it occurred twice.

Step 4.4: Bring On the Icons

Just like with your own custom elements, if you want to use any of Polymer's elements, you first need to import them. You're going to want to make use of iron-icon from Polymer's Iron Elements collection. Composition for the win!

Add this code with the other import statements near the top of the <icon-toggle> Dart file:

lib/icon_toggle.dart

import 'package:polymer_elements/iron_icons.dart';
import 'package:polymer_elements/iron_icon.dart';

This will import both the default icon set Polymer provides as well as the <iron-icon> element.

Now is the time to put something visible into the local DOM of <icon-toggle>. Add this to your element's <template>:

lib/icon_toggle.html

<iron-icon icon="polymer"></iron-icon>

<iron-icon> is a custom element that renders an icon. Here, it's hard-coded to use an icon named "polymer" from the iron-icons set.

If you run your code now, you should see something like this on the screen:

icon-toggle

With a few styles, you can give a pressed icon a new look. Add these CSS rules to the <style> tag:

lib/icon_toggle.html

iron-icon {
  fill: rgba(0, 0, 0, 0);
  stroke: black;
}

:host([pressed]) iron-icon {
  fill: black;
}

The <iron-icon> tag uses an SVG icon. The fill and stroke properties are SVG-specific CSS properties. They set the fill and outline color for the icon. Note that the rgba() function is used to set a transparent fill.

The :host() function matches the host element if the selector inside the parentheses matches the host element. In this case, [pressed] is a standard CSS attribute selector, so this rule matches when the <icon-toggle> has a pressed attribute set on it.

Run it again and you can see the difference. Remember, the second one has the pressed attribute.

icon-toggle

Step 4.5: The Ties That Bind

Right now, <icon-toggle> is boring and aloof, like a bad date. It always displays the Polymer logo and completely ignores you if you try to touch it. In this step, you'll use a little data binding magic to correct that first problem.

Customizing the Icon

First, find your use of <iron-icon> in your custom element and replace the hard-coded name with [[toggleIcon]].

lib/icon_toggle.html

<iron-icon icon="[[toggleIcon]]"></iron-icon>

toggleIcon is a property you will add to your IconToggle class. [[toggleIcon]] (with square brackets) is a one-way data binding. It will link the toggleIcon property in the Dart class with the icon property of <iron-icon>. Changes made to toggleIcon will automatically propagate to <iron-icon>.

Next, define a toggleIcon property in the IconToggle class. Place this line just above the IconToggle.created() constructor:

lib/icon_toggle.dart

@property
String toggleIcon;

Declaring a property with the @property annotation allows a user of your custom element to set it with markup. Change the <icon-toggle> elements in your main app to make use of this new functionality:

lib/main_app.html

<icon-toggle toggle-icon="star"></icon-toggle>
<icon-toggle toggle-icon="star" pressed></icon-toggle>

Note that the attribute name in the HTML markup is slightly different from the Dart property name. Polymer represents camelCase properties using dash-case attributes. Dashes are legal characters in HTML identifiers, and HTML doesn't honor case, making this translation necessary. To learn more about this, see Property name to attribute name mapping.

Run index.html now, and you'll be seeing stars.

icon-toggle

Press Here

While you're adding properties, you might as well throw in one more. Supporting the pressed attribute on <icon-toggle> is a bit more complicated, so for that one, you'll use the @Property annotation (capital P). Add this new property beneath the toggleIcon property.

lib/icon_toggle.dart

@Property(notify: true, reflectToAttribute: true)
bool pressed = false;

Here, you're creating a Boolean property with a default value of false.

The notify parameter tells Polymer to generate change events when the property value changes. This lets the change be observed by other nodes.

The reflectToAttribute parameter tells Polymer to update the corresponding attribute in the HTML markup when the property changes. In this case, whenever IconToggle's pressed property is true, the pressed attribute will be present on <icon-toggle>, and vice versa. This lets you style the element using a CSS attribute selector, like icon-toggle[pressed].

Note: The notify and reflectToAttribute parameters may seem similar: They both make the element's state visible to the outside world. reflectToAttribute changes the state in the DOM tree, so that it's visible to CSS and Dart's querySelector() methods. notify makes state changes observable outside the element, either using Dart event handlers or Polymer two-way data binding.

Step 4.6: Toggle Me

So far, precious little toggling has been going on for an element with "toggle" right in the name. Time to fix that.

To add an event listener to the host element, use the @Listen annotation. Add this event-handler method after the ready() method in your IconToggle class.

lib/icon_toggle.dart

@Listen('tap')
void toggle(Event event, Map detail) {
  set('pressed', !pressed);
}

All Polymer event handlers take an HTML Event and a Dart Map object containing details pertinent to the event in question These methods always return nothing (void).

The @Listen annotation takes the event type as an argument. The tap event is generated by Polymer's gesture system when the user clicks or taps on a target with a mouse or finger.

In order to make sure the Polymer property change events are fired, you must set the value of pressed using PolymerElement's set() method, one of many helper functions that IconToggle inherited when it extended that class.

If you run your app now, you should find that the star icons will toggle in response to clicks/taps.

Step 4.7: Getting Fancy

But why did you go to all that trouble to prepare the pressed property for two-way binding? Here's why.

Add another few elements to your main app, inside the element's <template>.

lib/main_app.html

<h3>Data-bound icon-toggle</h3>
<div>{{message(isFav)}}</div>
<icon-toggle toggle-icon="favorite" pressed="{{isFav}}"></icon-toggle>

The <div> contains a computed binding. Every time a property called isFav changes, the message() method will be called, and its result will be inserted as the <div>'s child node.

And how does isFav get changed? <icon-toggle> does it through the miracle of two-way binding. With pressed="{{isFav}}", you're telling Polymer that whenever the pressed property of <icon-toggle> becomes true (or false), so should isFav on <main-app>. Of course, each time that happens, the computed binding {{message(isFav)}} is also triggered.

Now, to define the code behind these bindings, add isFav and message() to your main app class. You can ignore or delete the class's existing properties and methods.

lib/main_app.dart

@property
bool isFav = false;

@reflectable
String message(bool fav) => fav ? "You really like me!" : "Do you like me?";

You might remember from early in the code lab that any class method that will be an event handler or be involved in data bindings needs to be annotated with @reflectable. Dart's function arrow syntax helps you keep message() brief, negating the need for the return keyword or curly braces for the body.

Run the app again to see this:

icon-toggle

Clicking/tapping the "favorite" icon (the heart) should change the message above it. No doubt, you agree that it's adorable.

Step 4.8: A Cure for a Black Heart

If you've followed instructions to the letter so far, everything is working, but the black heart is possibly a bit off-putting. You can fix that with a touch of styling.

Your custom element's local DOM prevents users of your element from styling its internals by accident, but what if you want to let them in? Maybe that user is a special someone, and he/she/it is offended by and suspicious of your guarded, secretive nature. In this step, you'll use some custom CSS properties to go along with your custom elements.

Custom CSS properties use the var() function, like so:

syntax

background-color: var(--my-custom-property, defaultValue);

Here, --my-custom-property is a custom property name, which must always start with the double-dash (--), and defaultValue is an optional CSS value that's used if the custom property isn't set.

Edit the <style> tag of <icon-toggle>, replacing the existing fill and stroke values with new, shiny, custom properties.

lib/icon_toggle.html

iron-icon {
  fill: var(--icon-toggle-color, rgba(0, 0, 0, 0));
  stroke: var(--icon-toggle-outline-color, black);
}

:host([pressed]) iron-icon {
  fill: var(--icon-toggle-pressed-color, black);
}

Because of the default values, this change alone doesn't affect the appearance of your icons, but now the user of your custom element can alter these values.

In this code lab, the user of <icon-toggle> is <main-app>, so open that up and set the values in the app's <style> tag.

lib/main_app.html

:host {
  display: block;
  --icon-toggle-color: lightgrey;
  --icon-toggle-outline-color: black;
  --icon-toggle-pressed-color: red;
}

icon-toggle

You could set the custom values on each <icon-toggle> individually, but if you set them on <main-app> itself, using :host, all child instances of <icon-toggle> will inherit them. If you decide you don't want red stars, you could do something more like this, instead:

icon-toggle[toggle-icon="favorite"] {
  --icon-toggle-color: lightgrey;
  --icon-toggle-outline-color: black;
  --icon-toggle-pressed-color: red;
}

That one will only target heart icons.

Step 5: Self-Congratulation

You did it! You've created a couple of custom HTML elements, and one of them even has a basic UI, API, and custom styling properties. Tell all your friends! Make T-shirts advertising your prowess. Share this code lab on Facebook, Google+, and Reddit. Fly your Polymer flag high, because you're now part of a revolution.