Files
polymer/PRIMER.md
2015-01-17 08:47:33 -06:00

31 KiB
Raw Blame History

WORK IN PROGRESS

Polymer 0.8 Primer

Feature Layering

Polymer 0.8 is currently layered into 3 sets of features provided as 3 discrete HTML imports, such that an individual element developer can depend on a version of Polymer whose feature set matches their tastes/needs. For authors who opt out of the more opinionated local DOM or data-binding features, their element's dependencies would not be payload- or runtime-burdened by these higher-level features, to the extent that a user didn't depend on other elements using those features on that page. That said, all features are designed to have low runtime cost when unused by a given element.

Higher layers depend on lower layers, and elements requiring lower layers will actually be imbued with features of the highest-level version of Polymer used on the page (those elements would simply not use/take advantage of those features). This provides a good tradeoff between element authors being able to avoid direct dependencies on unused features when their element is used standalone, while also allowing end users to mix-and-match elements created with different layers on the same page.

Below is a description of the current Polymer layers and included features, followed by individual feature guides.

polymer-micro.html

Bare-minum Custom Element sugaring

Feature Usage
Custom element constructor Polymer.Class({ … });
Custom element registration Polymer({ name: ..., … }};
Bespoke constructor support constructor: function() { … }
Basic lifecycle callbacks created, attached, detached, attributeChanged
Native HTML element extension extends: ‘…’
Publish API published: { … }
Attribute deserialization to property published: { <property>: <Type> }
Set boolean host attributes hostAttributes: [ … ]
Module registry modularize, using
Prototype Mixins mixins: [ … ]

polymer-mini.html

Custom Elements with Templates stamped into "local DOM"

Feature Usage
Template stamping into local DOM <dom-module><template>...</template></dom-module>
Light child (re-)distribution <content>
Local & light tree API localDom, lightDom
Top-down callback after distribution configure: function() { … }
Bottom-up callback after configuration ready: function() { … }

polymer.html (standard)

Custom elements with declarative data binding, events, and property nofication

Feature Usage
Local node marshalling this.$.<id>
Event listener setup listeners: { <node>.<event>: function, ... }
Annotated event listener setup <element on-[event]=”function”>
Key listener setup keyPresses: { '<cha]r>'
Property change callbacks bind: { <property>: function }
Declarative property binding <element prop=”{{property
Computed properties compute: { <property>: function(<property>) }
Path change notification setPath(<path>, <value>)
Utility functions toggleClass, toggleAttribute, fire, async, …
Attribute-based layout layout.html (layout horizontal flex ...)

Polymer Micro Features

Custom Element Constructor

The most basic Polymer API is Polymer.Class({...}), which takes an object expressing the prototype of your custom element, chains it to Polymer's Base prototype (which provides value-add features described below), and returns a constructor that can be passed to document.regsterElement() to register your element with the HTML parser, and after which can be used to instantiate new instances of your element via code.

The only requirement for the prototype passed to Polymer.Class is that is property specifies the HTML tag name the element will be registered as.

Example:

var MyElement = Polymer.Class({

	is: 'my-element',

	// See below for lifecycle callbacks
	created: function() {
		this.innerHTML = 'My element!';
	}
	
});

document.registerElement('my-element', MyElement);

// Equivalent:
var el1 = new MyElement();
var el2 = document.createElement('my-element');

Polymer.Class is designed to provide similar ergonomics to a speculative future where an ES6 class may be defined and provided to document.registerElement to achieve the same effect.

Custom Element Registration

Because the vast majority of users will always want to register the custom element prototype generated by Polymer, Polymer provides a Polymer({ ... }) function that wraps calling Polymer.Class and document.registerElement.

Example:

MyElement = Polymer({

	is: 'my-element',

	// See below for lifecycle callbacks
	created: function() {
		this.innerHTML = 'My element!';
	}
	
});

var el1 = new MyElement();
var el2 = document.createElement('my-element');

Bespoke constructor support

While the standard Polymer.Class() and Polymer() functions return a basic constructor that can be used to instance the custom element, Polymer also supports providing a bespoke constructor function on the prototype that can, for example, accept arguments to configure the element. In that case, the constructor should generally call document.createElement(this.is) to construct the element and return the element instance after constructing it.

Example:

MyElement = Polymer({

	is: 'my-element',

	constructor: function(foo, bar) {
		var el = document.createElement(this.is);
		el.foo = foo;
		el.configureWithBar(bar);
		return el;
	},
	
	configureWithBar: function(bar) {
		...
	}
	
});

var el = new MyElement(42, 'octopus');

Native HTML element extension

Polymer 0.8 currently only supports extending native HTML elements (e.g. input, button, etc., as opposed to extending other custom elements). To extend a native HTML element, set the extends property to the tag name of the element to extend.

Example:

MyInput = Polymer({

	is: 'my-input',

	extends: 'input',
	
	created: function() {
		this.style.border = '1px solid red';
	}
	
});

var el1 = new MyInput();
console.log(el1 instanceof HTMLInputElement); // true

var el2 = document.createElement('input', 'my-input');
console.log(el2 instanceof HTMLInputElement); // true

Basic lifecycle callbacks

Polymer's Base prototype implements the standard Custom Element lifecycle callbacks to perform tasks necessary for Polymer's built-in features. The hooks in turn call shorter-named lifecycle methods on your prototype.

  • created instead of createdCallback
  • attached instead of attachedCallback
  • detached instead of detachedCallback
  • attributeChanged instead of attributeChangedCallback

You can always fallback to using the low-level methods if you wish (in other words, you could simply implement createdCallback in your prototype).

Example:

MyElement = Polymer({

	is: 'my-element',

	create: function() {
		console.log(this.localName + '#' + this.id + ' was created');
	},
	
	attached: function() {
		console.log(this.localName + '#' + this.id + ' was attached');
	},
	
	detached: function() {
		console.log(this.localName + '#' + this.id + ' was detached');
	},
	
	attributeChanged: function(name, type) {
		console.log(this.localName + '#' + this.id + ' attribute ' + name +
			' was changed to ' + this.getAttribute(name));
	}
	
});

Polymer.Base also implements registerCallback, which will be called by Polymer() to allow Polymer.Base to supply a layering system for Polymer abstractions.

See the section on configuring elements for a more in-depth description of the practical uses of each callback.

Published API

Placing an object-valued published property on your prototype allows you to define metadata regarding your Custom Element's API, which can then be accessed by an API for use by other Polymer features.

By itself, the published feature doesn't do anything. It only provides API for asking questions about these special properties (see featues below for details).

Example:

Polymer({

  is: 'x-custom',

  published: {
    user: String,
    isHappy: Boolean,
    count: {
      type: Number,
      readOnly: true,
      notify: true
    }
  },

  created: function() {
    this.innerHTML = 'Hello World, I am a <b>Custom Element!</b>';
  }

});

Remember that the fields assigned to count, such as readOnly and notify don't do anything by themselves, it requires other features to give them life, and may depend on which layer of Polymer is in use.

Attribute deserialization

If an attribute matches a property listed in the published object, the attribute value will be assigned to a property of the same name on the element instance. Attribute values (always strings) will be automatically converted to the published type when assigned to the property. If no other published options are specified for a property, the type (specified using the type constructor, e.g. Object, String, etc.) can be set directly as the value of the property in the published object; otherwise it should be provided as the value to the type key in the published configuration object.

The type system includes support for Object values expressed as JSON, or Date objects expressed as any Date-parsable string representation. Boolean properties set based on the existence of the attribute: if the attribute exists at all, its value is true, regardless of its string-value (and the value is only false if the attribute does not exist).

Example:

<script>

  Polymer({

    is: 'x-custom',

    published: {
      user: String,
      manager: {
      	type: Boolean,
      	notify: true
      }
    },

    created: function() {
      // render
      this.innerHTML = 'Hello World, my user is ' + (this.user || 'nobody') + '.\n' +
      	'This user is ' + (this.manager ? '' : 'not') + ' a manager.';
    }

  });

</script>

<x-custom user="Scott" manager></x-custom>

Boolean host attributes

A list of attribute names to be applied to instances of the custom element can be provided as space-separated strings in the hostAttributes property. These will simply be set on the element during creation. This is intended for "boolean" attributes only, such as common layout attributes used by the layout.html CSS; attribute values cannot be supplied at this time.

Example:

<script>

  Polymer({

    is: 'x-custom',
    
    hostAttribute: 'layout horizontal fit'
	
  });

</script>

After creation:

<x-custom layout horizontal fit></x-custom>

Module registry

Polymer provides and internally uses a JavaScript "module registry" to organize library code defined outside the context of a custom element prototype, and may be used to organize user code when convenient as well. The registry is responsible for storing and retrieving JS modules by name. As this facility does not provide dependency loading, it is the responsibility of the user to HTMLImport files containing any dependent modules before use.

Modules are registered using the modulate global function, passing a name to register and a factory function that returns the module:

fun-support.html

<script>
modulate('FunSupport', function() {

	return {
		makeElementFun = function(el) {
			el.style.border = 'border: 20px dotted fuchsia;';
		}
	};

});
</script>

A list of module dependencies can be specified as the second parameter, which will be provided to the factory function:

fun-support.html

<script>
modulate('FunSupport', ['Squid', 'Octopus'], function(squid, octopus) {

	// use squid & octopus
	return ...;

});
</script>

Modules are requested using the using global function, passing a list of dependencies:

my-element.html

<!-- Load module dependency -->
<link rel="import" href="fun-support.html">

<script>
// Use module dependency
using(['FunSupport', ...], function(funSupport, ...) {

	MyElement = Polymer({

		is: 'my-element',

		created: function() {
			funSupport.makeElementFun(this);
		}
		
	});

});
</script>

Prototype mixins

Polymer will "mixin" objects specified in a mixin array into the prototype. This can be useful for adding common code between multiple elements.

The current mixin feature in 0.8 is basic; it simply loops over properties in the provided object and adds property descriptors for those on the prototype (such that set/get accessors are copied in addition to properties and functions). Note that there is currently no support for publishing properties or hooking lifecycle callbacks directly via mixins. The general pattern is for the mixin to supply functions to be called by the target element as part of its usage contract (and should be documented as such). These limitations will likely be revisited in the future.

The module registry should generally be used for registering mixins. Mixins registered with the Polymer module registry may be referred to by String name without needing to expliitly request the module via using. Otherwise, values in the mixin array should be an Object reference (generally retrieved via using).

Example: fun-mixin.html

modulate('FunMixin', function() {

	return {
		funCreatedCallback: function() {
			this.makeElementFun();
		},
		
		makeElementFun = function() {
			this.style.border = 'border: 20px dotted fuchsia;';
		}
	};

});

Example: my-element.html

<link rel="import" href="fun-mixin.html">

<script>
Polymer({

	is: 'my-element',
	
	mixins: ['FunMixin'],

	created: function() {
		this.funCreatedCallback();
	}
	
});
</script>

Polymer Mini Layer

Template stamping into local DOM

Light child (re-)distribution

Configure callback

Ready callback

Polymer Standard Layer

Local node marshalling

Polymer automatically builds a map of instance nodes stamped into its local DOM, to provide convenient access to frequently used nodes without the need to query for (and memoize) them manually. Any node specified in the element's template with an id is stored on the this.$ hash by id.

Example:

<template>

  Hello World from <span id="name"></span>!

</template>

<script>

  Polymer({

    is: 'x-custom',

    created: function() {
      this.$.name.textContent = this.name;
    }

  });

</script>

Event listener setup

Event listeners can be added to the host element by providing an object-valued listeners property that maps events to event handler function names. A <id>.<event> syntax is supported for adding listeners on local-DOM children by id.

Example:

<template>

  <button id="button">Click Me</button>

</template>

<script>

  Polymer({

    is: 'x-custom',
    
    listeners: {
    	'click': 'warnAction',
    	'button.click': 'kickAction'
    },
    
    warnAction: function(e) {
    	alert("Don't click me, click the button!");
    },

    kickAction: function(e) {
      alert('The "' + e.target.textContent + '" button was clicked.');
      return true;
    }

  });

</script>

Annotated event listener setup

For adding event listeners to local-DOM children, a more convenient on-<event> annotation syntax is supported directly in the template. This often eliminates the need to give an element an id solely for the purpose of binding an event listener.

Example:

<template>

  <button on-click="kickAction">Kick Me</button>

</template>

<script>

  Polymer({

    is: 'x-custom',

    kickAction: function() {
      alert('Ow!');
    }

  });

</script>

Key listener setup

Polymer will automatically listen for keypress events and call handlers specified in the keyPresses object, which maps key codes to handler functions. The key may either be specified as a keyboard code or one of several convenience strings supported:

  • ESC_KEY
  • ENTER_KEY
  • LEFT
  • UP
  • RIGHT
  • DOWN

Example:

Polymer({

  is: 'x-custom',
  
  keyPresses: {
  	'ESC_KEY': 'exitCurrentMode',
  	88: 'handleXKeyPress'
  },
  
  exitCurrentMode: function(e) {
  	...
  },

  handleXKeyPress: function(e) {
  	...
  }

});

Property change callbacks

Custom element properties may be observed for changes by specifying an object-valued bind property that maps element properties to chagne handler names. When the property changes, the change handler will be called with the new and old values.

Example:

Polymer({

  is: 'x-custom',

  published: {
    disabled: Boolean
  },
  
  bind: {
    disabled: 'disabledChanged'
  },
  
  disabledChanged: function(newValue, oldValue) {
    this.toggleClass('disabled', newValue);
    this.highlight = true;
  },
  
  highlightChanged: function() {
    this.classList.add('highlight');
    setTimeout(function() {
      this.classList.remove('highlight');
    }, 300);
  }

});

Note as in the example above, change handlers can be bound to properties that are not necessarily published.

Property change observation is achieved in Polymer by installing setters on the custom element prototype for properties with registered interest (as opposed to observation via Object.observe or dirty checking, for example).

Observing changes to object sub-properties is also supported via the bind object, by specifying a full (e.g. user.manager.name) or partial path (user.*).

Example:

Polymer({

  is: 'x-custom',

  published: {
    user: Object
  },
  
  bind: {
    'user.manager.*': 'userManagerChanged'
  },
  
  userManagerChanged: function(newValue, oldValue, path) {
    if (path) {
      // sub-property of user.manager changed
      console.log('manager ' + path.split('.').pop() + ' changed to ' + newValue);
    } else {
      // user.manager object itself changed
      console.log('new manager name is ' + newValue.name);
    }
  }

});

Note that observing changes to paths (object sub-properties) is dependent on one of two requirements: either the value at the path in question changed via a Polymer property binding to another element, or the value was changed using the setPath API, which provides the required notification to elements with registered interest.

Declarative property binding

Basic property binding

Properties of the custom element may be bound into text content or properties of local DOM elements using binding annotations in the template.

To bind to textContent, the binding annotation must currently span the entire content of the tag:

<template>

	<!-- Supported -->
	First: <span>{{firstName}}</span><br>
	Last: <span>{{lastName}}</span>

	<!-- Not currently supported! -->
	<div>First: {{firstName}}</div>
	<div>Last: {{lastName}}</div>

</template>

<script>

	Polymer({
	
	  is: 'user-view',

	  published: {
	    firstName: String,
	    lastName: String
	  }
	
	});

</script>

<user-view firstName="Samuel" lastName="Adams"></user-view>

To bind to properties, the binding annotation should be provided as the value to an attribute with the same name of the JS property to bind to:

<template>

	<user-view firstName="{{user.first}}" last="{{user.last}}"></user-view>

</template>

<script>

	Polymer({
	
	  is: 'main-view',

	  published: {
	    user: Object
	  }
	
	});

</script>

As in the exmaple above, paths to object sub-properties may also be specified in templates. See Binding to structured data for details.

Note that while HTML attributes are used to specify bindings, values are assigned directly to JS properties, not to the HTML attributes of the elements.

Note that currently binding to style is a special case which results in the value being set to style.cssText.

One-way vs. Two-way binding

Polymer supports cooperative two-way binding between elements, allowing elements that "produce" data or changes to data to propagate those changes upwards to hosts when desired.

When a Polymer elements changs a property that was "published" as part of its public API with the notify flag set to true, it automatically fires a non-bubbling DOM event to indicate those changes to interested hosts. These events follow a naming convention of <property>-changed, and contain a value property in the event.detail object indicating the new value.

As such, one could attach an on-<property>-changed listener to an element to be notified of changes to such properties, set the event.detail.value to a property on itself, and take necessary actions based on the new value. However, given this is a common pattern, bindings using "curly-braces" (e.g. {{property}}) will automatically perform this upwards binding automatically without the user needing to perform those tasks. This can be defeated by using "square-brace" syntax (e.g. [[property]]), which results in only one-way (downward) data-binding.

To summarize, two-way data-binding is achieved when both the host and the child agree to participate, satisfying these three conditions:

  1. The host must use curly-brace {{property}} syntax. Square-brace [[property]] syntax results in one-way downward binding, regardless of the notify state of the child's property.
  2. The child property being bound to must be published with the notify flag set to true (or otherwise send a <propety>-changed custom event). If the property being bound is not published or if the notify flag is not set, only one-way (downward) binding will occur.
  3. The child property being bound to must not published with the readOnly flag set to true. If the child property is notify: true and readOnly:true, and the host binding uses curly-brace syntax, the binding will effectively be one-way (upward).

Example 1: Two-way binding


<script>
	Polymer({
		is: 'custom-element',
		published: {
			prop: {
				type: String,
				notify: true
			}
		}
	});
</script>

...

<template>
	<!-- changes to `value` propagate downward to `prop` on child -->
	<!-- changes to `prop` propagate upward to `value` on host  -->
	<custom-element prop="{{value}}"></custom-element>
</template

Example 2: One-way binding (downward)


<script>
	Polymer({
		is: 'custom-element',
		published: {
			prop: {
				type: String,
				notify: true
			}
		}
	});
</script>

...

<template>
	<!-- changes to `value` propagate downward to `prop` on child -->
	<!-- changes to `prop` are ignored by host due to square-bracket syntax -->
	<custom-element prop="[[value]]"></custom-element>
</template

Example 2: One-way binding (downward)


<script>
	Polymer({
		is: 'custom-element',
		published: {
			prop: String    // no `notify:true`!
		}
	});
</script>

...

<template>
	<!-- changes to `value` propagate downward to `prop` on child -->
	<!-- changes to `prop` are not notified to host due to notify:falsey -->
	<custom-element prop="{{value}}"></custom-element>
</template

Example 3: One-way binding (upward)


<script>
	Polymer({
		is: 'custom-element',
		published: {
			prop: String,
			notify: true,
			readOnly: true
		}
	});
</script>

...

<template>
	<!-- changes to `value` are ignored by child due to readOnly:true -->
	<!-- changes to `prop` propagate upward to `value` on host  -->
	<custom-element prop="{{value}}"></custom-element>
</template

Example 4: Error / non-sensical state


<script>
	Polymer({
		is: 'custom-element',
		published: {
			prop: String,
			notify: true,
			readOnly: true
		}
	});
</script>

...

<template>
	<!-- changes to `value` are ignored by child due to readOnly:true -->
	<!-- changes to `prop` are ignored by host due to square-bracket syntax -->
	<!-- binding serves no purpose -->
	<custom-element prop="[[value]]"></custom-element>
</template

Binding to structured data

Sub-properties of objects may be two-way bound to properties of custom elements as well by specifying the path of interest to the binding annotation.

Example:

<template>
	<div>{{user.manager.name}}</div>
	<user-element user="{{user}}"></user-element>
</template>

As with change handlers for paths, bindings to paths (object sub-properties) are dependent on one of two requirements: either the value at the path in question changed via a Polymer property binding to another element, or the value was changed using the setPath API, which provides the required notification to elements with registered interest, as discussed below.

Note that path bindings are distinct from property bindings in a subtle way: when a property's value changes, an assignment must occur for the value to propagate to the property on the element at the other side of the binding. However, if two elements are bound to the same path of a shared object and the value at that path changes (via a property binding or via setPath), the value seen by both elements actually changes with no additional assignment necessary, by virtue of it being a property on a shared object reference. In this case, the element who changed the path must notify the system so that other elements who have registered interest in the same path may take side effects. However, there is no concept of one-way binding in this case, since there is no concept of propagation. That is, all bindings and change handlers for the same path will always be notified and update when the value of the path changes.

Path change notification

Two-way data-binding and observation of paths in Polymer is achieved using a similar strategy to the one described above for 2-way property binding: When a sub-property of a published Object changes, an element fires a non-bubbling <property>-path-changed DOM event with a detail.path value indicating the path on the object that changed. Elements that have registered interest in that object (either via binding or change handler) may then take side effects based on knowledge of the path having changed. Finally, those elements will forward the notification on to any children they have bound the object to, and if the element published the root object for the path that changed on its API, it will also fire a new <propety>-path-changed event appropriately. Through this method, a notification will reach any part of the tree that has registered interest in that path so that side effects occur.

This system "just works" to the extent that changes to object sub-properties occur as a result of being bound to a notifying custom element property that changed. However, often imperative code needs to "poke" at an object's sub-properties directly. As we avoid more sophisticated observation mechanisms such as Object.observe or dirty-checking in order to achieve the best startup and runtime performance cross-platform for the most common use cases, changing an object's sub-properties directly requires cooperation from the user.

Specifically, Polymer provides two API's that allow such changes to be notified to the system: notifyPath(path, value) and setPath(path, value).

Example:

<template>
	<div>{{user.manager.name}}</div>
</template>

<script>
	Polymer({

		is: 'custom-element',
		
		reassignManager: function(newManager) {
			this.user.manager = newManager;
			// Notification required for binding to update!
			this.notifyPath('user.manager', this.user.manager);
		}

	});
</script>

Since in the majority of cases, notifyPath will be called directly after an assignment, a convenience function setPath is provided that performs both actions:

		reassignManager: function(newManager) {
			this.setPath('user.manager', newManager);
		}

Expressions in binding annotations

Currently the only binding expression supported in Polymer binding annotations is negation using !:

Example:

<template>
	<div hidden="{{!enabled}}></div>
</template>

Computed properties

Polymer supports virtual properties whose values are calculated from other properties. Computed properties can be defined by providing an object-valued computed property on the prototype that maps property names to computing functions. The name of the function to compute the value is provided as a string with dependent properties as arguments in parenthesis. Only one dependency is supported at this time.

<template>
	My name is <span>{{fullName}}</span>
</template>
<script>
    Polymer({
    
       computed: {
         // when `user` changes `computeFullName` is called and the 
         // value it returns is stored as `fullName`
         fullName: 'computeFullName(user)',
       },
    
       computeFullName: function(user) {
         return user.firstName + ' ' + user.lastName;
       }
    
      ...
    
    });
</script>

Utility Functions

Polymer's Base prototype provides a set of useful convenience/utility functions for instances to use. See API documentation for more details.

  • toggleClass: function(name, bool, [node])
  • toggleAttribute: function(name, bool, [node])
  • attributeFollows: function(name, neo, old)
  • fire: function(type, [detail], [onNode], [bubbles], [cancelable])
  • async: function(method)
  • queryHost: function(node)
  • transform: function(node, transform)
  • translate3d: function(node, x, y, z)
  • importHref: function(href, onload, onerror)

Attribute-based layout

CSS is notoriously verbose for doing even the most basic layout tasks, and so Polymer provides a useful set of layout CSS that can be applied using very terse HTML attribues (as opposed to CSS classes), which we find greatly improves the readability of markup. Below is a quick cheatsheet of attributes available; refer to layout.html directly for details.

General:

  • block
  • hidden
  • relative
  • fit
  • fullbleed

Flexbox:

  • layout horizontal, layout vertical
    • inline
    • reverse
    • wrap
    • wrap-reverse
    • start
    • center
    • end
    • start-justified
    • center-justified
    • end-justified
    • around-justified
    • center-center
    • justified

Flexbox children:

  • flex
    • auto
    • none
    • one .. twelve
  • self-start
  • self-center
  • self-end
  • self-stretch

Migration Notes

Styling

Self / Child Configuration

Binding limitations

Current limitations that are on the backlog for evaluation/improvement:

  • no sub-textContent binding
  • no attribute binding
  • no good class/style

Compound property effects

Structured data and path notification

Array notification

Mixins / Inheritance