Wednesday, June 15, 2016

Using Polymer with Closure Compiler - Part 1: Type Information

This is the first post in a 3-part series about using Polymer with Closure Compiler

Introduction

Closure Compiler has long been practically the only JavaScript compiler to be able to rename properties (using the ADVANCED optimization level). In addition, it offers an impressive amount of static analysis and type checking while still writing in native ECMAScript. However, with the adoption of frameworks with data-bound templates such as Angular, the power of Closure Compiler has been significantly reduced because the template references are external to the compiler.

With Polymer, it is possible to maintain a high degree of property renaming and type checking while still utilizing the power of data-bound HTML templates. Finding information on how to use the two together has been difficult thus far.

This post explains how type information in the compiler is created from Polymer element definitions and how to utilize them. Part 2 concentrates on how to obtain optimal renaming of Polymer elements and part 3 will detail how to rename data-binding references in the HTML templates consistently with the element properties.

The Polymer Pass

Closure Compiler has a pass specifically written to process Polymer element definitions and produce the correct type information. The pass is enabled by specifying the --polymer_pass command line flag. The pass allows the rest of the compiler to properly understand Polymer types.

Polymer element definitions contain both standard properties and can also declare properties on a special properties object. It can be confusing to understand the difference. Both will end up as properties on the created class’ prototype. However, if you have a property which does not need the extra abilities of the properties object, where does it go? The official guidance has been if the property is part of the public API for an element, it should be defined on the properties object. However, with Closure Compiler, it’s not quite so cut-and-dry.

Declared Properties - Children of the properties Object

Declared properties biggest advantage is how they work behind the scenes. Polymer attaches them using getters and setters so that any change to the property automatically updates data-bound expressions. However, because these elements can also be serialized as attributes, referenced from CSS and are generally considered external interfaces, the compiler will never rename them. The Polymer pass of the compiler creates an external interface and marks the Polymer element as implementing that interface. This blocks renaming without incurring the loss of type checking that happens with properties which are quoted.

Standard Properties

Standard properties on the other hand are potentially renamable (the compiler will use its standard strategies to determine whether it is safe to rename the property or not). In fact, one method to prevent template references from being broken by renaming is to quote all standard properties used in data-binding expressions. This is less than ideal and part 3 of the series will describe how to avoid this. Standard properties are still accessible from outside of the component and can also be considered part of the public API of an element, but they retain the ability to be renamed. However, because they are not defined with Polymer’s getters and setters, you must either use the Polymer set method to make changes to the property or use either notifyPath or notifySplices to inform Polymer that a change has already occurred. The next post in the series talks about how to use these methods with renamed properties.

Behaviors

Polymer behaviors are essentially a mixin. Since the compiler needs explicit knowledge of behavior implementation, type definitions are copied onto the element. The Polymer pass of Closure Compiler automatically creates sub entries for each behavior method. There are a some not-so-obvious implementation details however:

  1. Behaviors must be defined as object literals - just like a Polymer element definition.
  2. Behaviors must be annotated with the special @polymerBehavior annotation.
  3. Methods of a behavior should be annotated with @this {!PolymerElement} so that the compiler knows the correct type of the this keyword.
  4. Behaviors must be defined as a global type name - and cannot be aliased.

Element Type Names

Most Polymer examples call the Polymer function without using the return value. In these cases, the Polymer pass will automatically create a type name for the element from the tag name. The tag name is converted from a hyphenated name to an upper camel case name and the word Element is appended. For instance, the compiler sees the foo-bar element definition and creates a global FooBarElement type. This allows references in other elements to type cast the value.

var fooBarElement =
    /** @type {FooBarElement} */(this.$$('foo-bar'));
Authors may also choose to assign their own type name. To do that, simply use the return value of the Polymer function.

myNamespace.FooBar = Polymer({is:'foo-bar'});
The assigned name will now be the type name:

var fooBarElement =
    /** @type {myNamespace.FooBar} */(this.$$('foo-bar'));
Because type names must be globally accessible, the Polymer pass will only recognize this assignment in the global scope or if the name is a qualified namespace assignment.

Summary

This post describes how Closure Compiler processes Polymer element definitions. In many cases, the compiler will automatically process and infer type information. However, Polymer elements must still be written in a way which is compatible with the restrictions imposed by Closure Compiler. Don’t assume that any element you find will automatically be compatible. If you find one that isn’t, may I suggest a pull request?

No comments:

Post a Comment