Build Reusable Widgets for the Web with Polymer and Dart

Don't wait for browsers to implement Web Components, try them today

Web Components, the family of new web specifications for reusable and encapsulated widgets, are coming to a browser near you. Thanks to Polymer, a new type of library for the web built on top of Web Components, and Dart, a new structured language and libraries for modern web development, you can build custom HTML elements that encapsulate style, structure, and behavior.

fig_1

I’m personally a fan of Polymer.dart, a Dart port of Polymer. If you want to use JavaScript, you can use the original Polymer library, though some of the details are different. Both Polymer and Polymer.dart are under heavy development, and the engineers behind the projects are looking for feedback.

In this article, I’ll show you how to build a simple auto-complete widget as a custom element. As a user types into a field, a list is searched by prefix. The user can keep typing for a more exact match, use up and down to select, or use their mouse.

fig_2

Aside: You can download the code featured in this article from my Github account.

Getting started

To get started, I recommend using Dart Editor, which you can download for Mac, Linux, and Windows. Create a new project without any generated content.

fig_3

Use the pub (because you play darts in a pub!) package manager to download and install the Polymer.dart package. Inside of your new project, create a pubspec.yaml file with the following contents:

name: my_autocomplete_demo
description: A fun demo of polymer.dart.
dependencies:
  polymer: any

Dart Editor installs packages declared in pubspec.yaml, along with transitive dependencies, when you save the pubspec.yaml file. On initial save of pubspec.yaml, a new packages folder appears in your project.

fig_4

Creating the HTML for the custom element

With the dependencies installed, it’s time to create the custom element. Create a web directory in your project, and create an auto_complete.html file inside of web. Here we go!

It’s fitting that you use HTML to define new HTML tags. Inside of auto_complete.html, declare the auto-complete tag:

<polymer-element name="auto-complete">

</polymer-element>

Tip: custom element names must have a dash (-) in their name.

It’s also fitting that you can build a custom element out of other HTML tags. Use the <template> tag to define the internal structure of an auto-complete element. This internal structure is generally hidden from the main DOM of the page, thanks to a new web specification called Shadow DOM. True encapsulation for the structure of a custom element!

Aside: The Shadow DOM is a deep topic, out of scope for this article. When you’re ready to dive in, check out Shadow DOM 101.

Add the following template for auto-complete:

<polymer-element name="auto-complete">
  <template>
    <input type="text" value="{{search}}" on-key-up="keyup">
  </template>
</polymer-element>

Polymer looks for {{ and }} to configure data binding. When a user types into the input field, the search property on the data model that backs the Polymer element is also updated. If the search property is ever updated from the code, the input field’s value also changes. No need to find elements on a page, wire up event listeners, or other tedious work; Polymer takes care of it for you.

Polymer elements can also declare event handlers. In the above code, the keyup method is run when the key-up event is fired from the input field.

Creating the Dart class

The search property and the keyup method are defined inside a Dart class that backs this custom element. Each instance of the auto-complete tag gets an instance of the AutoCompleteElement class. Let’s create the Dart class now.

Inside of the web directory, create a new file called auto_complete.dart. Here are the initial contents:

import 'package:polymer/polymer.dart';
import 'dart:html';

@CustomTag('auto-complete')
class AutoCompleteElement extends PolymerElement with ObservableMixin {
  @observable String search;

  keyup(KeyboardEvent e, var detail, Node target) {
    // ...
  }
}

Dart code is familiar to most developers with a background in JavaScript, Java, C#, C++, ActionScript3, etc. What follows is a quick tour:

  • import – import a Dart library. Some libraries come from packages, others come built into the Dart SDK, other libraries can be found simply by a URI.
  • class – declares a class. Dart is an object-oriented, class-based, single-inheritance language.
  • with – applies a mixin. Dart supports mixins, a way to introduce functionality to a class without interfering with the inheritance hierarchy. Here we mixin observability, required for data binding.
  • @CustomTag – a metadata annotation. Specifically, used to bind this class to the auto-complete custom element.
  • @observable – tells Polymer.dart to watch this field for changes and to keep it in sync via data binding.
  • keyup – a class method.
  • var – shorthand for dynamic, or the “unknown” type. Dart is an optionally typed language. When a method parameter might be more than one type, use var.

The properties of the Dart class are in scope for data binding expressions in the . The search in auto_complete.dart is the same as the {{search}} in the auto_complete.html file.

Linking the custom element HTML to the Dart class

Use a <script> tag to link the Dart class to the <polymer-element>. Open auto_complete.html and add this code:

  <script type="application/dart" src="auto_complete.dart"></script>
  </template>
</polymer-element>

Creating the application HTML

The index.html page is your “app”: it imports and uses the element. The index page is also responsible for any initialization or bootstrap code for Polymer.dart.

Custom elements are reusable through HTML imports, a new specification to include and reuse HTML documents in other HTML documents.

Tip: Learn more about HTML imports by reading the HTML imports spec.

Inside of the web directory, create a new file named index.html and add the following code:

<!DOCTYPE html>

<html>
  <head>
    <title>Auto-complete Demo</title>
    <link rel="import" href="auto_complete.html">
    <script src="packages/polymer/boot.js"><script>
  </head>

  <body>
    <auto-complete></auto-complete>
  </body>
</html>

Don’t look now, but you’ve just connected all the dots between a custom element, its Dart class, and the application page that uses it. There’s a minor issue of the auto-complete not actually doing anything, so let’s make it do stuff!

Using the DOM for search data

There are plenty of ways for an auto-complete element to find data to search through. Sticking with a theme of using HTML wherever we can, and for simplicity, I wanted to use the DOM to declare all search options.

Open index.html and add the following code for the search terms:

<auto-complete>
  <ul class="data-source">
   <li>ActionScript
   <li>AppleScript</li>
   <li>Asp</li>
   <li>BASIC</li>
   <li>C</li>
   <li>C++</li>
   <li>Clojure</li>
   <li>COBOL</li>
   <li>ColdFusion</li>
   <li>Dart</li>
 <!-- … more options … --> 
   </ul> 
</auto-complete>

The Dart class is responsible for reading the list of search options and storing them for searching. The created() lifecycle method is useful here. Open auto_complete.dart and add the following code:

  

  final List haystack = [];

  void created() {
    super.created();
    
    var dataSource = host.query('.data-source') as UListElement;
    if (dataSource == null) {
      print("WARNING: expected to find a .data-source as a child");
      return;
    }
    
    dataSource.children.forEach((LIElement e) {
      if (e is! LIElement) return;
      haystack.add(e.text);
    });
    
    bindProperty(this, const Symbol('search'), _performSearch);
  }

The code above finds all children of .data-source and adds their text to a list of options named haystack.

Also important is the bindProperty call, which tells Polymer.dart to run the _performSearch method any time the search property changes. Speaking of performing a search, let’s make that work.

Performing a search and displaying results

As the previous step added, any time the search property changes, a search is performed. Store search results into an observable list. Add the results property to the class:

final List results = toObservable([]);

When items are added to, or removed from, the results list, the HTML is kept in sync via data binding.

Now, add the search logic. Open auto_complete.dart and add the _performSearch method:

_performSearch() {
  results.clear();
  if (search.trim().isEmpty) return;
  String lower = search.toLowerCase();
  results.addAll(haystack.where((String term) {
    return term.toLowerCase().startsWith(lower);
  }));
}

First, the results are cleared. The haystack (all search options) is filtered by matching the search term as a prefix against all search options. All matched terms are added to results.

Displaying the results is made easy thanks to data binding and the <template> tag with the repeat attribute. Open auto_complete.html and add the following code:

   
    <input type="text" value="{{search}}" on-key-up="keyup">
    <template if="{{results.length > 0}}">
      <ul>
        <template repeat="{{result in results}}">
          <li on-click="select">{{result}}</li>
        </template>
      </ul>
    </template>
  </template>
    <script type="application/dart" src="auto_complete.dart"></script>
</polymer-element>

When results is updated, any expressions that use results are evaluated. The <template repeat> renders an <li> for each result. An on-click is configured for each search result so a user can click a result to select it.

Finishing up

You’ve now seen most of the major code for <auto-complete>, but there are still a few details and features to add. For example, check out how the select method is implemented. I encourage you to check out the code in Github for the complete application.

The complete application also supports keyboard navigation up and down through the displayed results, as well as hitting Enter to select an option.

Summary

This article showed you how to use Polymer.dart to create a custom element that performs an auto-complete search. You used a <polymer-element> to define the structure of the element, set up data binding with its backing Dart class, and declare event handlers. You also used the <template> tag for conditionals and loops. You created a Dart class to implement the search functionality, store the search options, and the search value.

Web Components are set to revolutionize the way we build web apps. You get true encapsulation and reusability, backed by emerging web standards. Don’t wait for browsers to implement Web Components; try them today with Polymer and Dart.

tags: , ,