| Index: pkg/polymer/lib/src/instance.dart
 | 
| ===================================================================
 | 
| --- pkg/polymer/lib/src/instance.dart	(revision 37373)
 | 
| +++ pkg/polymer/lib/src/instance.dart	(working copy)
 | 
| @@ -4,18 +4,43 @@
 | 
|  
 | 
|  part of polymer;
 | 
|  
 | 
| -/// Use this annotation to publish a field as an attribute. For example:
 | 
| +/// Use this annotation to publish a field as an attribute.
 | 
|  ///
 | 
| +/// You can also use [PublishedProperty] to provide additional information,
 | 
| +/// such as automatically syncing the property back to the attribute.
 | 
| +///
 | 
| +/// For example:
 | 
| +///
 | 
|  ///     class MyPlaybackElement extends PolymerElement {
 | 
|  ///       // This will be available as an HTML attribute, for example:
 | 
| +///       //
 | 
|  ///       //     <my-playback volume="11">
 | 
| +///       //
 | 
| +///       // It will support initialization and data-binding via <template>:
 | 
| +///       //
 | 
| +///       //     <template>
 | 
| +///       //       <my-playback volume="{{x}}">
 | 
| +///       //     </template>
 | 
| +///       //
 | 
| +///       // If the template is instantiated or given a model, `x` will be
 | 
| +///       // used for this field and updated whenever `volume` changes.
 | 
|  ///       @published double volume;
 | 
| +///
 | 
| +///       // This will be available as an HTML attribute, like above, but it
 | 
| +///       // will also serialize values set to the property to the attribute.
 | 
| +///       // In other words, attributes['volume2'] will contain a serialized
 | 
| +///       // version of this field.
 | 
| +///       @PublishedProperty(reflect: true) double volume2;
 | 
|  ///     }
 | 
| +///
 | 
|  const published = const PublishedProperty();
 | 
|  
 | 
|  /// An annotation used to publish a field as an attribute. See [published].
 | 
|  class PublishedProperty extends ObservableProperty {
 | 
| -  const PublishedProperty();
 | 
| +  /// Whether the property value should be reflected back to the HTML attribute.
 | 
| +  final bool reflect;
 | 
| +
 | 
| +  const PublishedProperty({this.reflect: false});
 | 
|  }
 | 
|  
 | 
|  /// Use this type to observe a property and have the method be called when it
 | 
| @@ -47,6 +72,17 @@
 | 
|    const ObserveProperty(this._names);
 | 
|  }
 | 
|  
 | 
| +
 | 
| +/// Base class for PolymerElements deriving from HtmlElement.
 | 
| +///
 | 
| +/// See [Polymer].
 | 
| +class PolymerElement extends HtmlElement with Polymer, Observable {
 | 
| +  PolymerElement.created() : super.created() {
 | 
| +    polymerCreated();
 | 
| +  }
 | 
| +}
 | 
| +
 | 
| +
 | 
|  /// The mixin class for Polymer elements. It provides convenience features on
 | 
|  /// top of the custom elements web standard.
 | 
|  ///
 | 
| @@ -96,44 +132,77 @@
 | 
|      if (extendsTag != null) poly.attributes['extends'] = extendsTag;
 | 
|      if (template != null) poly.append(template);
 | 
|  
 | 
| -    new JsObject.fromBrowserObject(poly).callMethod('init');
 | 
| +    // TODO(jmesserly): conceptually this is just:
 | 
| +    //     new JsObject.fromBrowserObject(poly).callMethod('init')
 | 
| +    //
 | 
| +    // However doing it that way hits an issue with JS-interop in IE10: we get a
 | 
| +    // JsObject that wraps something other than `poly`, due to improper caching.
 | 
| +    // By reusing _polymerElementProto that we used for 'register', we can
 | 
| +    // then call apply on it to invoke init() with the correct `this` pointer.
 | 
| +    JsFunction init = _polymerElementProto['init'];
 | 
| +    init.apply([], thisArg: poly);
 | 
|    }
 | 
|  
 | 
| -  /// The one syntax to rule them all.
 | 
| -  static final BindingDelegate _polymerSyntax =
 | 
| -      new PolymerExpressionsWithEvents();
 | 
| +  // Note: these are from src/declaration/import.js
 | 
| +  // For now proxy to the JS methods, because we want to share the loader with
 | 
| +  // polymer.js for interop purposes.
 | 
| +  static Future importElements(Node elementOrFragment) {
 | 
| +    var completer = new Completer();
 | 
| +    js.context['Polymer'].callMethod('importElements',
 | 
| +        [elementOrFragment, () => completer.complete()]);
 | 
| +    return completer.future;
 | 
| +  }
 | 
|  
 | 
| -  static int _preparingElements = 0;
 | 
| +  static Future importUrls(List urls) {
 | 
| +    var completer = new Completer();
 | 
| +    js.context['Polymer'].callMethod('importUrls',
 | 
| +        [urls, () => completer.complete()]);
 | 
| +    return completer.future;
 | 
| +  }
 | 
|  
 | 
| -  static final Completer _ready = new Completer();
 | 
| +  static final Completer _onReady = new Completer();
 | 
|  
 | 
|    /// Future indicating that the Polymer library has been loaded and is ready
 | 
|    /// for use.
 | 
| -  static Future get onReady => _ready.future;
 | 
| +  static Future get onReady => _onReady.future;
 | 
|  
 | 
| -  PolymerDeclaration _declaration;
 | 
| -
 | 
|    /// The most derived `<polymer-element>` declaration for this element.
 | 
| -  PolymerDeclaration get declaration => _declaration;
 | 
| +  PolymerDeclaration get element => _element;
 | 
| +  PolymerDeclaration _element;
 | 
|  
 | 
| -  Map<String, StreamSubscription> _observers;
 | 
| +  /// Deprecated: use [element] instead.
 | 
| +  @deprecated PolymerDeclaration get declaration => _element;
 | 
| +
 | 
| +  Map<String, StreamSubscription> _namedObservers;
 | 
| +  List<Iterable<Bindable>> _observers = [];
 | 
| +
 | 
|    bool _unbound; // lazy-initialized
 | 
| -  _Job _unbindAllJob;
 | 
| +  PolymerJob _unbindAllJob;
 | 
|  
 | 
|    CompoundObserver _propertyObserver;
 | 
| +  bool _readied = false;
 | 
|  
 | 
| -  bool get _elementPrepared => _declaration != null;
 | 
| +  /// Returns the object that should be used as the event controller for
 | 
| +  /// event bindings in this element's template. If set, this will override the
 | 
| +  /// normal controller lookup.
 | 
| +  // TODO(jmesserly): type seems wrong here? I'm guessing this should be any
 | 
| +  // kind of model object. Also, should it be writable by anyone?
 | 
| +  Polymer eventController;
 | 
|  
 | 
| -  bool get applyAuthorStyles => false;
 | 
| -  bool get resetStyleInheritance => false;
 | 
| -  bool get alwaysPrepare => false;
 | 
| -  bool get preventDispose => false;
 | 
| +  bool get hasBeenAttached => _hasBeenAttached;
 | 
| +  bool _hasBeenAttached = false;
 | 
|  
 | 
| -  BindingDelegate syntax = _polymerSyntax;
 | 
| +  /// Gets the shadow root associated with the corresponding custom element.
 | 
| +  ///
 | 
| +  /// This is identical to [shadowRoot], unless there are multiple levels of
 | 
| +  /// inheritance and they each have their own shadow root. For example,
 | 
| +  /// this can happen if the base class and subclass both have `<template>` tags
 | 
| +  /// in their `<polymer-element>` tags.
 | 
| +  // TODO(jmesserly): should expose this as an immutable map.
 | 
| +  // Similar issue as $.
 | 
| +  final Map<String, ShadowRoot> shadowRoots =
 | 
| +      new LinkedHashMap<String, ShadowRoot>();
 | 
|  
 | 
| -  /// Shadow roots created by [parseElement]. See [getShadowRoot].
 | 
| -  final _shadowRoots = new HashMap<String, ShadowRoot>();
 | 
| -
 | 
|    /// Map of items in the shadow root(s) by their [Element.id].
 | 
|    // TODO(jmesserly): various issues:
 | 
|    // * wrap in UnmodifiableMapView?
 | 
| @@ -146,50 +215,81 @@
 | 
|    @reflectable final Map<String, Element> $ =
 | 
|        new ObservableMap<String, Element>();
 | 
|  
 | 
| -  /// Gets the shadow root associated with the corresponding custom element.
 | 
| -  ///
 | 
| -  /// This is identical to [shadowRoot], unless there are multiple levels of
 | 
| -  /// inheritance and they each have their own shadow root. For example,
 | 
| -  /// this can happen if the base class and subclass both have `<template>` tags
 | 
| -  /// in their `<polymer-element>` tags.
 | 
| -  // TODO(jmesserly): Polymer does not have this feature. Reconcile.
 | 
| -  ShadowRoot getShadowRoot(String customTagName) => _shadowRoots[customTagName];
 | 
| +  /// Use to override the default syntax for polymer-elements.
 | 
| +  /// By default this will be null, which causes [instanceTemplate] to use
 | 
| +  /// the template's bindingDelegate or the [element.syntax], in that order.
 | 
| +  PolymerExpressions get syntax => null;
 | 
|  
 | 
| +  bool get _elementPrepared => _element != null;
 | 
| +
 | 
| +  /// Retrieves the custom element name. It should be used instead
 | 
| +  /// of localName, see: https://github.com/Polymer/polymer-dev/issues/26
 | 
| +  String get _name {
 | 
| +    if (_element != null) return _element.name;
 | 
| +    var isAttr = attributes['is'];
 | 
| +    return (isAttr == null || isAttr == '') ? localName : isAttr;
 | 
| +  }
 | 
| +
 | 
| +  /// By default the data bindings will be cleaned up when this custom element
 | 
| +  /// is detached from the document. Overriding this to return `true` will
 | 
| +  /// prevent that from happening.
 | 
| +  bool get preventDispose => false;
 | 
| +
 | 
|    /// If this class is used as a mixin, this method must be called from inside
 | 
|    /// of the `created()` constructor.
 | 
|    ///
 | 
|    /// If this class is a superclass, calling `super.created()` is sufficient.
 | 
|    void polymerCreated() {
 | 
| -    if (this.ownerDocument.window != null || alwaysPrepare ||
 | 
| -        _preparingElements > 0) {
 | 
| -      prepareElement();
 | 
| +    var t = nodeBind(this).templateInstance;
 | 
| +    if (t != null && t.model != null) {
 | 
| +      window.console.warn('Attributes on $_name were data bound '
 | 
| +          'prior to Polymer upgrading the element. This may result in '
 | 
| +          'incorrect binding types.');
 | 
|      }
 | 
| -  }
 | 
| +    prepareElement();
 | 
|  
 | 
| -  /// Retrieves the custom element name by inspecting the host node.
 | 
| -  String get _customTagName {
 | 
| -    var isAttr = attributes['is'];
 | 
| -    return (isAttr == null || isAttr == '') ? localName : isAttr;
 | 
| +    // TODO(sorvell): replace when ShadowDOMPolyfill issue is corrected
 | 
| +    // https://github.com/Polymer/ShadowDOM/issues/420
 | 
| +    if (!isTemplateStagingDocument(ownerDocument) || _hasShadowDomPolyfill) {
 | 
| +      makeElementReady();
 | 
| +    }
 | 
|    }
 | 
|  
 | 
| +  /// *Deprecated* use [shadowRoots] instead.
 | 
| +  @deprecated
 | 
| +  ShadowRoot getShadowRoot(String customTagName) => shadowRoots[customTagName];
 | 
| +
 | 
|    void prepareElement() {
 | 
| -    // Dart note: get the _declaration, which also marks _elementPrepared
 | 
| -    _declaration = _getDeclaration(_customTagName);
 | 
| -    // do this first so we can observe changes during initialization
 | 
| -    observeProperties();
 | 
| +    if (_elementPrepared) {
 | 
| +      window.console.warn('Element already prepared: $_name');
 | 
| +      return;
 | 
| +    }
 | 
| +    // Dart note: get the corresponding <polymer-element> declaration.
 | 
| +    _element = _getDeclaration(_name);
 | 
| +    // install property storage
 | 
| +    createPropertyObserver();
 | 
| +    // TODO (sorvell): temporarily open observer when created
 | 
| +    openPropertyObserver();
 | 
|      // install boilerplate attributes
 | 
|      copyInstanceAttributes();
 | 
|      // process input attributes
 | 
|      takeAttributes();
 | 
|      // add event listeners
 | 
|      addHostListeners();
 | 
| -    // guarantees that while preparing, any
 | 
| -    // sub-elements are also prepared
 | 
| -    _preparingElements++;
 | 
| +  }
 | 
| +
 | 
| +  makeElementReady() {
 | 
| +    if (_readied) return;
 | 
| +    _readied = true;
 | 
| +
 | 
| +    // TODO(sorvell): We could create an entry point here
 | 
| +    // for the user to compute property values.
 | 
|      // process declarative resources
 | 
| -    parseDeclarations(_declaration);
 | 
| -    // decrement semaphore
 | 
| -    _preparingElements--;
 | 
| +    parseDeclarations(_element);
 | 
| +    // TODO(sorvell): CE polyfill uses unresolved attribute to simulate
 | 
| +    // :unresolved; remove this attribute to be compatible with native
 | 
| +    // CE.
 | 
| +    attributes.remove('unresolved');
 | 
|      // user entry point
 | 
|      ready();
 | 
|    }
 | 
| @@ -197,14 +297,30 @@
 | 
|    /// Called when [prepareElement] is finished.
 | 
|    void ready() {}
 | 
|  
 | 
| -  void enteredView() {
 | 
| +  /// domReady can be used to access elements in dom (descendants,
 | 
| +  /// ancestors, siblings) such that the developer is enured to upgrade
 | 
| +  /// ordering. If the element definitions have loaded, domReady
 | 
| +  /// can be used to access upgraded elements.
 | 
| +  ///
 | 
| +  /// To use, override this method in your element.
 | 
| +  void domReady() {}
 | 
| +
 | 
| +  void attached() {
 | 
|      if (!_elementPrepared) {
 | 
| -      prepareElement();
 | 
| +      // Dart specific message for a common issue.
 | 
| +      throw new StateError('polymerCreated was not called for custom element '
 | 
| +          '$_name, this should normally be done in the .created() if Polymer '
 | 
| +          'is used as a mixin.');
 | 
|      }
 | 
| -    cancelUnbindAll(preventCascade: true);
 | 
| +
 | 
| +    cancelUnbindAll();
 | 
| +    if (!hasBeenAttached) {
 | 
| +      _hasBeenAttached = true;
 | 
| +      async((_) => domReady());
 | 
| +    }
 | 
|    }
 | 
|  
 | 
| -  void leftView() {
 | 
| +  void detached() {
 | 
|      if (!preventDispose) asyncUnbindAll();
 | 
|    }
 | 
|  
 | 
| @@ -220,21 +336,13 @@
 | 
|    void parseDeclaration(Element elementElement) {
 | 
|      var template = fetchTemplate(elementElement);
 | 
|  
 | 
| -    var root = null;
 | 
|      if (template != null) {
 | 
| -      if (_declaration.element.attributes.containsKey('lightdom')) {
 | 
| -        lightFromTemplate(template);
 | 
| -      } else {
 | 
| -        root = shadowFromTemplate(template);
 | 
| -      }
 | 
| +      var root = shadowFromTemplate(template);
 | 
| +
 | 
| +      var name = elementElement.attributes['name'];
 | 
| +      if (name == null) return;
 | 
| +      shadowRoots[name] = root;
 | 
|      }
 | 
| -
 | 
| -    // Dart note: the following code is to support the getShadowRoot method.
 | 
| -    if (root is! ShadowRoot) return;
 | 
| -
 | 
| -    var name = elementElement.attributes['name'];
 | 
| -    if (name == null) return;
 | 
| -    _shadowRoots[name] = root;
 | 
|    }
 | 
|  
 | 
|    /// Return a shadow-root template (if desired), override for custom behavior.
 | 
| @@ -242,17 +350,28 @@
 | 
|        elementElement.querySelector('template');
 | 
|  
 | 
|    /// Utility function that stamps a `<template>` into light-dom.
 | 
| -  Node lightFromTemplate(Element template) {
 | 
| +  Node lightFromTemplate(Element template, [Node refNode]) {
 | 
|      if (template == null) return null;
 | 
| +
 | 
| +    // TODO(sorvell): mark this element as an event controller so that
 | 
| +    // event listeners on bound nodes inside it will be called on it.
 | 
| +    // Note, the expectation here is that events on all descendants
 | 
| +    // should be handled by this element.
 | 
| +    eventController = this;
 | 
| +
 | 
|      // stamp template
 | 
|      // which includes parsing and applying MDV bindings before being
 | 
|      // inserted (to avoid {{}} in attribute values)
 | 
|      // e.g. to prevent <img src="images/{{icon}}"> from generating a 404.
 | 
|      var dom = instanceTemplate(template);
 | 
|      // append to shadow dom
 | 
| -    append(dom);
 | 
| -    // perform post-construction initialization tasks on shadow root
 | 
| -    shadowRootReady(this, template);
 | 
| +    if (refNode != null) {
 | 
| +      append(dom);
 | 
| +    } else {
 | 
| +      insertBefore(dom, refNode);
 | 
| +    }
 | 
| +    // perform post-construction initialization tasks on ahem, light root
 | 
| +    shadowRootReady(this);
 | 
|      // return the created shadow root
 | 
|      return dom;
 | 
|    }
 | 
| @@ -269,18 +388,8 @@
 | 
|    /// shadowRootReady with a node other than a ShadowRoot such as with `this`.
 | 
|    ShadowRoot shadowFromTemplate(Element template) {
 | 
|      if (template == null) return null;
 | 
| -    // cache elder shadow root (if any)
 | 
| -    var elderRoot = this.shadowRoot;
 | 
|      // make a shadow root
 | 
|      var root = createShadowRoot();
 | 
| -
 | 
| -    // Provides ability to traverse from ShadowRoot to the host.
 | 
| -    // TODO(jmessery): remove once we have this ability on the DOM.
 | 
| -    _shadowHost[root] = this;
 | 
| -
 | 
| -    // migrate flag(s)(
 | 
| -    root.applyAuthorStyles = applyAuthorStyles;
 | 
| -    root.resetStyleInheritance = resetStyleInheritance;
 | 
|      // stamp template
 | 
|      // which includes parsing and applying MDV bindings before being
 | 
|      // inserted (to avoid {{}} in attribute values)
 | 
| @@ -289,17 +398,19 @@
 | 
|      // append to shadow dom
 | 
|      root.append(dom);
 | 
|      // perform post-construction initialization tasks on shadow root
 | 
| -    shadowRootReady(root, template);
 | 
| +    shadowRootReady(root);
 | 
|      // return the created shadow root
 | 
|      return root;
 | 
|    }
 | 
|  
 | 
| -  void shadowRootReady(Node root, Element template) {
 | 
| +  void shadowRootReady(Node root) {
 | 
|      // locate nodes with id and store references to them in this.$ hash
 | 
|      marshalNodeReferences(root);
 | 
| -    // TODO(jmesserly): port this
 | 
| -    // set up pointer gestures
 | 
| -    // PointerGestures.register(root);
 | 
| +
 | 
| +    // set up polymer gestures
 | 
| +    if (_PolymerGestures != null) {
 | 
| +      _PolymerGestures.callMethod('register', [root]);
 | 
| +    }
 | 
|    }
 | 
|  
 | 
|    /// Locate nodes with id and store references to them in [$] hash.
 | 
| @@ -331,13 +442,13 @@
 | 
|    }
 | 
|  
 | 
|    void copyInstanceAttributes() {
 | 
| -    _declaration._instanceAttributes.forEach((name, value) {
 | 
| +    _element._instanceAttributes.forEach((name, value) {
 | 
|        attributes.putIfAbsent(name, () => value);
 | 
|      });
 | 
|    }
 | 
|  
 | 
|    void takeAttributes() {
 | 
| -    if (_declaration._publishLC == null) return;
 | 
| +    if (_element._publishLC == null) return;
 | 
|      attributes.forEach(attributeToProperty);
 | 
|    }
 | 
|  
 | 
| @@ -374,7 +485,7 @@
 | 
|    /// Return the published property matching name, or null.
 | 
|    // TODO(jmesserly): should we just return Symbol here?
 | 
|    smoke.Declaration propertyForAttribute(String name) {
 | 
| -    final publishLC = _declaration._publishLC;
 | 
| +    final publishLC = _element._publishLC;
 | 
|      if (publishLC == null) return null;
 | 
|      //console.log('propertyForAttribute:', name, 'matches', match);
 | 
|      return publishLC[name];
 | 
| @@ -395,23 +506,21 @@
 | 
|      return null;
 | 
|    }
 | 
|  
 | 
| -  void reflectPropertyToAttribute(PropertyPath path) {
 | 
| -    if (path.length != 1) throw new ArgumentError('path must be length 1');
 | 
| -
 | 
| +  void reflectPropertyToAttribute(String path) {
 | 
|      // TODO(sjmiles): consider memoizing this
 | 
|      // try to intelligently serialize property value
 | 
| -    final propValue = path.getValueFrom(this);
 | 
| +    final propValue = new PropertyPath(path).getValueFrom(this);
 | 
|      final serializedValue = serializeValue(propValue);
 | 
|      // boolean properties must reflect as boolean attributes
 | 
|      if (serializedValue != null) {
 | 
| -      attributes['$path'] = serializedValue;
 | 
| +      attributes[path] = serializedValue;
 | 
|        // TODO(sorvell): we should remove attr for all properties
 | 
|        // that have undefined serialization; however, we will need to
 | 
|        // refine the attr reflection system to achieve this; pica, for example,
 | 
|        // relies on having inferredType object properties not removed as
 | 
|        // attrs.
 | 
|      } else if (propValue is bool) {
 | 
| -      attributes.remove('$path');
 | 
| +      attributes.remove(path);
 | 
|      }
 | 
|    }
 | 
|  
 | 
| @@ -420,97 +529,88 @@
 | 
|    ///
 | 
|    ///     templateBind(template).createInstance(this, polymerSyntax);
 | 
|    ///
 | 
| -  /// Where polymerSyntax is a singleton `PolymerExpressions` instance from the
 | 
| -  /// [polymer_expressions](https://pub.dartlang.org/packages/polymer_expressions)
 | 
| -  /// package.
 | 
| +  /// Where polymerSyntax is a singleton [PolymerExpressions] instance.
 | 
|    ///
 | 
|    /// You can override this method to change the instantiation behavior of the
 | 
|    /// template, for example to use a different data-binding syntax.
 | 
| -  DocumentFragment instanceTemplate(Element template) =>
 | 
| -      templateBind(template).createInstance(this, syntax);
 | 
| +  DocumentFragment instanceTemplate(Element template) {
 | 
| +    var syntax = this.syntax;
 | 
| +    var t = templateBind(template);
 | 
| +    if (syntax == null && t.bindingDelegate == null) {
 | 
| +      syntax = element.syntax;
 | 
| +    }
 | 
| +    var dom = t.createInstance(this, syntax);
 | 
| +    registerObservers(getTemplateInstanceBindings(dom));
 | 
| +    return dom;
 | 
| +  }
 | 
|  
 | 
| -  // TODO(jmesserly): Polymer does not seem to implement the oneTime flag
 | 
| -  // correctly. File bug.
 | 
| -  Bindable bind(String name, Bindable bindable, {bool oneTime: false}) {
 | 
| -    // note: binding is a prepare signal. This allows us to be sure that any
 | 
| -    // property changes that occur as a result of binding will be observed.
 | 
| -    if (!_elementPrepared) prepareElement();
 | 
| -
 | 
| +  Bindable bind(String name, bindable, {bool oneTime: false}) {
 | 
|      var decl = propertyForAttribute(name);
 | 
|      if (decl == null) {
 | 
|        // Cannot call super.bind because template_binding is its own package
 | 
|        return nodeBindFallback(this).bind(name, bindable, oneTime: oneTime);
 | 
|      } else {
 | 
| -      // clean out the closets
 | 
| -      unbind(name);
 | 
|        // use n-way Polymer binding
 | 
| -      var observer = bindProperty(decl.name, bindable);
 | 
| +      var observer = bindProperty(decl.name, bindable, oneTime: oneTime);
 | 
| +      // NOTE: reflecting binding information is typically required only for
 | 
| +      // tooling. It has a performance cost so it's opt-in in Node.bind.
 | 
| +      if (enableBindingsReflection && observer != null) {
 | 
| +         // Dart note: this is not needed because of how _PolymerBinding works.
 | 
| +         //observer.path = bindable.path_;
 | 
| +         _recordBinding(name, observer);
 | 
| +      }
 | 
| +      var reflect = _element._reflect;
 | 
|  
 | 
| -      // reflect bound property to attribute when binding
 | 
| -      // to ensure binding is not left on attribute if property
 | 
| -      // does not update due to not changing.
 | 
| -      // Dart note: we include this patch:
 | 
| -      // https://github.com/Polymer/polymer/pull/319
 | 
| -
 | 
| -      // TODO(jmesserly): polymer has the path_ in their observer object, should
 | 
| -      // we use that too instead of allocating it here?
 | 
| -      reflectPropertyToAttribute(new PropertyPath([decl.name]));
 | 
| -      return bindings[name] = observer;
 | 
| +      // Get back to the (possibly camel-case) name for the property.
 | 
| +      var propName = smoke.symbolToName(decl.name);
 | 
| +      if (reflect != null && reflect.contains(propName)) {
 | 
| +        reflectPropertyToAttribute(propName);
 | 
| +      }
 | 
| +      return observer;
 | 
|      }
 | 
|    }
 | 
|  
 | 
| +  _recordBinding(String name, observer) {
 | 
| +    if (bindings == null) bindings = {};
 | 
| +    this.bindings[name] = observer;
 | 
| +  }
 | 
| +
 | 
| +  bindFinished() => makeElementReady();
 | 
| +
 | 
|    Map<String, Bindable> get bindings => nodeBindFallback(this).bindings;
 | 
| +  set bindings(Map value) { nodeBindFallback(this).bindings = value; }
 | 
| +
 | 
|    TemplateInstance get templateInstance =>
 | 
|        nodeBindFallback(this).templateInstance;
 | 
|  
 | 
| -  void unbind(String name) => nodeBindFallback(this).unbind(name);
 | 
| -
 | 
| +  // TODO(sorvell): unbind/unbindAll has been removed, as public api, from
 | 
| +  // TemplateBinding. We still need to close/dispose of observers but perhaps
 | 
| +  // we should choose a more explicit name.
 | 
|    void asyncUnbindAll() {
 | 
|      if (_unbound == true) return;
 | 
| -    _unbindLog.fine('[$localName] asyncUnbindAll');
 | 
| -    _unbindAllJob = _runJob(_unbindAllJob, unbindAll, Duration.ZERO);
 | 
| +    _unbindLog.fine('[$_name] asyncUnbindAll');
 | 
| +    _unbindAllJob = scheduleJob(_unbindAllJob, unbindAll);
 | 
|    }
 | 
|  
 | 
|    void unbindAll() {
 | 
|      if (_unbound == true) return;
 | 
| -
 | 
| -    unbindAllProperties();
 | 
| -    nodeBindFallback(this).unbindAll();
 | 
| -
 | 
| -    var root = shadowRoot;
 | 
| -    while (root != null) {
 | 
| -      _unbindNodeTree(root);
 | 
| -      root = root.olderShadowRoot;
 | 
| -    }
 | 
| +    closeObservers();
 | 
| +    closeNamedObservers();
 | 
|      _unbound = true;
 | 
|    }
 | 
|  
 | 
| -  void cancelUnbindAll({bool preventCascade}) {
 | 
| +  void cancelUnbindAll() {
 | 
|      if (_unbound == true) {
 | 
| -      _unbindLog.warning(
 | 
| -          '[$localName] already unbound, cannot cancel unbindAll');
 | 
| +      _unbindLog.warning('[$_name] already unbound, cannot cancel unbindAll');
 | 
|        return;
 | 
|      }
 | 
| -    _unbindLog.fine('[$localName] cancelUnbindAll');
 | 
| +    _unbindLog.fine('[$_name] cancelUnbindAll');
 | 
|      if (_unbindAllJob != null) {
 | 
|        _unbindAllJob.stop();
 | 
|        _unbindAllJob = null;
 | 
|      }
 | 
| -
 | 
| -    // cancel unbinding our shadow tree iff we're not in the process of
 | 
| -    // cascading our tree (as we do, for example, when the element is inserted).
 | 
| -    if (preventCascade == true) return;
 | 
| -    _forNodeTree(shadowRoot, (n) {
 | 
| -      if (n is Polymer) {
 | 
| -        (n as Polymer).cancelUnbindAll();
 | 
| -      }
 | 
| -    });
 | 
|    }
 | 
|  
 | 
| -  static void _unbindNodeTree(Node node) {
 | 
| -    _forNodeTree(node, (node) => nodeBind(node).unbindAll());
 | 
| -  }
 | 
| -
 | 
|    static void _forNodeTree(Node node, void callback(Node node)) {
 | 
|      if (node == null) return;
 | 
|  
 | 
| @@ -521,49 +621,46 @@
 | 
|    }
 | 
|  
 | 
|    /// Set up property observers.
 | 
| -  void observeProperties() {
 | 
| -    final observe = _declaration._observe;
 | 
| -    final publish = _declaration._publish;
 | 
| -
 | 
| -    // TODO(jmesserly): workaround for a dart2js compiler bug
 | 
| -    bool hasObserved = observe != null;
 | 
| -
 | 
| -    if (hasObserved || publish != null) {
 | 
| +  void createPropertyObserver() {
 | 
| +    final observe = _element._observe;
 | 
| +    if (observe != null) {
 | 
|        var o = _propertyObserver = new CompoundObserver();
 | 
| -      if (hasObserved) {
 | 
| -        for (var path in observe.keys) {
 | 
| -          o.addPath(this, path);
 | 
| +      // keep track of property observer so we can shut it down
 | 
| +      registerObservers([o]);
 | 
|  
 | 
| -          // TODO(jmesserly): on the Polymer side it doesn't look like they
 | 
| -          // will observe arrays unless it is a length == 1 path.
 | 
| -          observeArrayValue(path, path.getValueFrom(this), null);
 | 
| -        }
 | 
| -      }
 | 
| -      if (publish != null) {
 | 
| -        for (var path in publish.keys) {
 | 
| +      for (var path in observe.keys) {
 | 
| +        o.addPath(this, path);
 | 
|  
 | 
| -          if (!hasObserved || !observe.containsKey(path)) {
 | 
| -            o.addPath(this, path);
 | 
| -          }
 | 
| -        }
 | 
| +        // TODO(jmesserly): on the Polymer side it doesn't look like they
 | 
| +        // will observe arrays unless it is a length == 1 path.
 | 
| +        observeArrayValue(path, path.getValueFrom(this), null);
 | 
|        }
 | 
| -      o.open(notifyPropertyChanges);
 | 
|      }
 | 
|    }
 | 
|  
 | 
| +  void openPropertyObserver() {
 | 
| +    if (_propertyObserver != null) {
 | 
| +      _propertyObserver.open(notifyPropertyChanges);
 | 
| +    }
 | 
| +    // Dart note: we need an extra listener.
 | 
| +    // see comment on [_propertyChange].
 | 
| +    if (_element._publish != null) {
 | 
| +      changes.listen(_propertyChange);
 | 
| +    }
 | 
| +  }
 | 
|  
 | 
|    /// Responds to property changes on this element.
 | 
|    void notifyPropertyChanges(List newValues, Map oldValues, List paths) {
 | 
| -    final observe = _declaration._observe;
 | 
| -    final publish = _declaration._publish;
 | 
| +    final observe = _element._observe;
 | 
|      final called = new HashSet();
 | 
|  
 | 
|      oldValues.forEach((i, oldValue) {
 | 
| +      final newValue = newValues[i];
 | 
| +
 | 
| +      // Date note: we don't need any special checking for null and undefined.
 | 
| +
 | 
|        // note: paths is of form [object, path, object, path]
 | 
| -      var path = paths[2 * i + 1];
 | 
| -      if (publish != null && publish.containsKey(path)) {
 | 
| -        reflectPropertyToAttribute(path);
 | 
| -      }
 | 
| +      final path = paths[2 * i + 1];
 | 
|        if (observe == null) return;
 | 
|  
 | 
|        var methods = observe[path];
 | 
| @@ -572,18 +669,46 @@
 | 
|        for (var method in methods) {
 | 
|          if (!called.add(method)) continue; // don't invoke more than once.
 | 
|  
 | 
| -        final newValue = newValues[i];
 | 
| -        // observes the value if it is an array
 | 
|          observeArrayValue(path, newValue, oldValue);
 | 
|          // Dart note: JS passes "arguments", so we pass along our args.
 | 
| +        // TODO(sorvell): call method with the set of values it's expecting;
 | 
| +        // e.g. 'foo bar': 'invalidate' expects the new and old values for
 | 
| +        // foo and bar. Currently we give only one of these and then
 | 
| +        // deliver all the arguments.
 | 
|          smoke.invoke(this, method,
 | 
|              [oldValue, newValue, newValues, oldValues, paths], adjust: true);
 | 
|        }
 | 
|      });
 | 
|    }
 | 
|  
 | 
| +  // Dart note: had to rename this to avoid colliding with
 | 
| +  // Observable.deliverChanges. Even worse, super calls aren't possible or
 | 
| +  // it prevents Polymer from being a mixin, so we can't override it even if
 | 
| +  // we wanted to.
 | 
| +  void deliverPropertyChanges() {
 | 
| +    if (_propertyObserver != null) {
 | 
| +      _propertyObserver.deliver();
 | 
| +    }
 | 
| +  }
 | 
| +
 | 
| +  // Dart note: this is not called by observe-js because we don't have
 | 
| +  // the mechanism for defining properties on our proto.
 | 
| +  // TODO(jmesserly): this has similar timing issues as our @published
 | 
| +  // properties do generally -- it's async when it should be sync.
 | 
| +  void _propertyChange(List<ChangeRecord> records) {
 | 
| +    for (var record in records) {
 | 
| +      if (record is! PropertyChangeRecord) continue;
 | 
| +
 | 
| +      final name = smoke.symbolToName(record.name);
 | 
| +      final reflect = _element._reflect;
 | 
| +      if (reflect != null && reflect.contains(name)) {
 | 
| +        reflectPropertyToAttribute(name);
 | 
| +      }
 | 
| +    }
 | 
| +  }
 | 
| +
 | 
|    void observeArrayValue(PropertyPath name, Object value, Object old) {
 | 
| -    final observe = _declaration._observe;
 | 
| +    final observe = _element._observe;
 | 
|      if (observe == null) return;
 | 
|  
 | 
|      // we only care if there are registered side-effects
 | 
| @@ -593,57 +718,62 @@
 | 
|      // if we are observing the previous value, stop
 | 
|      if (old is ObservableList) {
 | 
|        if (_observeLog.isLoggable(Level.FINE)) {
 | 
| -        _observeLog.fine('[$localName] observeArrayValue: unregister observer '
 | 
| -            '$name');
 | 
| +        _observeLog.fine('[$_name] observeArrayValue: unregister $name');
 | 
|        }
 | 
|  
 | 
| -      unregisterObserver('${name}__array');
 | 
| +      closeNamedObserver('${name}__array');
 | 
|      }
 | 
|      // if the new value is an array, being observing it
 | 
|      if (value is ObservableList) {
 | 
|        if (_observeLog.isLoggable(Level.FINE)) {
 | 
| -        _observeLog.fine('[$localName] observeArrayValue: register observer '
 | 
| -            '$name');
 | 
| +        _observeLog.fine('[$_name] observeArrayValue: register $name');
 | 
|        }
 | 
|        var sub = value.listChanges.listen((changes) {
 | 
|          for (var callback in callbacks) {
 | 
|            smoke.invoke(this, callback, [old], adjust: true);
 | 
|          }
 | 
|        });
 | 
| -      registerObserver('${name}__array', sub);
 | 
| +      registerNamedObserver('${name}__array', sub);
 | 
|      }
 | 
|    }
 | 
|  
 | 
| -  bool unbindProperty(String name) => unregisterObserver(name);
 | 
| +  void registerObservers(Iterable<Bindable> observers) {
 | 
| +    _observers.add(observers);
 | 
| +  }
 | 
|  
 | 
| -  void unbindAllProperties() {
 | 
| -    if (_propertyObserver != null) {
 | 
| -      _propertyObserver.close();
 | 
| -      _propertyObserver = null;
 | 
| +  void closeObservers() {
 | 
| +    _observers.forEach(closeObserverList);
 | 
| +    _observers = [];
 | 
| +  }
 | 
| +
 | 
| +  void closeObserverList(Iterable<Bindable> observers) {
 | 
| +    for (var o in observers) {
 | 
| +      if (o != null) o.close();
 | 
|      }
 | 
| -    unregisterObservers();
 | 
|    }
 | 
|  
 | 
|    /// Bookkeeping observers for memory management.
 | 
| -  void registerObserver(String name, StreamSubscription sub) {
 | 
| -    if (_observers == null) {
 | 
| -      _observers = new Map<String, StreamSubscription>();
 | 
| +  void registerNamedObserver(String name, StreamSubscription sub) {
 | 
| +    if (_namedObservers == null) {
 | 
| +      _namedObservers = new Map<String, StreamSubscription>();
 | 
|      }
 | 
| -    _observers[name] = sub;
 | 
| +    _namedObservers[name] = sub;
 | 
|    }
 | 
|  
 | 
| -  bool unregisterObserver(String name) {
 | 
| -    var sub = _observers.remove(name);
 | 
| +  bool closeNamedObserver(String name) {
 | 
| +    var sub = _namedObservers.remove(name);
 | 
|      if (sub == null) return false;
 | 
|      sub.cancel();
 | 
|      return true;
 | 
|    }
 | 
|  
 | 
| -  void unregisterObservers() {
 | 
| -    if (_observers == null) return;
 | 
| -    for (var sub in _observers.values) sub.cancel();
 | 
| -    _observers.clear();
 | 
| -    _observers = null;
 | 
| +  void closeNamedObservers() {
 | 
| +    if (_namedObservers == null) return;
 | 
| +    for (var sub in _namedObservers.values) {
 | 
| +      if (sub != null) sub.cancel();
 | 
| +    }
 | 
| +    _namedObservers.clear();
 | 
| +    _namedObservers = null;
 | 
|    }
 | 
|  
 | 
|    /// Bind the [name] property in this element to [bindable]. *Note* in Dart it
 | 
| @@ -656,7 +786,7 @@
 | 
|    ///       bindProperty(#myProperty,
 | 
|    ///           new PathObserver(this, 'myModel.path.to.otherProp'));
 | 
|    ///     }
 | 
| -  Bindable bindProperty(Symbol name, Bindable bindable) {
 | 
| +  Bindable bindProperty(Symbol name, Bindable bindable, {oneTime: false}) {
 | 
|      // Dart note: normally we only reach this code when we know it's a
 | 
|      // property, but if someone uses bindProperty directly they might get a
 | 
|      // NoSuchMethodError either from the getField below, or from the setField
 | 
| @@ -664,7 +794,7 @@
 | 
|      // difference from Polymer.js behavior.
 | 
|  
 | 
|      if (_bindLog.isLoggable(Level.FINE)) {
 | 
| -      _bindLog.fine('bindProperty: [$bindable] to [${localName}].[name]');
 | 
| +      _bindLog.fine('bindProperty: [$bindable] to [$_name].[$name]');
 | 
|      }
 | 
|  
 | 
|      // capture A's value if B's value is null or undefined,
 | 
| @@ -675,66 +805,39 @@
 | 
|        bindable.value = smoke.read(this, name);
 | 
|      }
 | 
|  
 | 
| -    // TODO(jmesserly): this will create another subscription.
 | 
| -    // It would be nice to have this reuse our existing _propertyObserver
 | 
| -    // created by observeProperties, to avoid more observation overhead.
 | 
| +    // TODO(jmesserly): we need to fix this -- it doesn't work like Polymer.js
 | 
| +    // bindings. https://code.google.com/p/dart/issues/detail?id=18343
 | 
| +    // apply Polymer two-way reference binding
 | 
| +    //return Observer.bindToInstance(inA, inProperty, observable,
 | 
| +    //    resolveBindingValue);
 | 
|      return new _PolymerBinding(this, name, bindable);
 | 
|    }
 | 
|  
 | 
|    /// Attach event listeners on the host (this) element.
 | 
|    void addHostListeners() {
 | 
| -    var events = _declaration._eventDelegates;
 | 
| +    var events = _element._eventDelegates;
 | 
|      if (events.isEmpty) return;
 | 
|  
 | 
|      if (_eventsLog.isLoggable(Level.FINE)) {
 | 
| -      _eventsLog.fine('[$localName] addHostListeners: $events');
 | 
| +      _eventsLog.fine('[$_name] addHostListeners: $events');
 | 
|      }
 | 
| -    addNodeListeners(this, events.keys, hostEventListener);
 | 
| -  }
 | 
|  
 | 
| -  void addNodeListeners(Node node, Iterable<String> events,
 | 
| -      void listener(Event e)) {
 | 
| -
 | 
| -    for (var name in events) {
 | 
| -      addNodeListener(node, name, listener);
 | 
| -    }
 | 
| +    // NOTE: host events look like bindings but really are not;
 | 
| +    // (1) we don't want the attribute to be set and (2) we want to support
 | 
| +    // multiple event listeners ('host' and 'instance') and Node.bind
 | 
| +    // by default supports 1 thing being bound.
 | 
| +    events.forEach((type, methodName) {
 | 
| +      // Dart note: the getEventHandler method is on our PolymerExpressions.
 | 
| +      var handler = element.syntax.getEventHandler(this, this, methodName);
 | 
| +      addEventListener(type, handler);
 | 
| +    });
 | 
|    }
 | 
|  
 | 
| -  void addNodeListener(Node node, String event, void listener(Event e)) {
 | 
| -    node.on[event].listen(listener);
 | 
| -  }
 | 
| -
 | 
| -  void hostEventListener(Event event) {
 | 
| -    // TODO(jmesserly): do we need this check? It was using cancelBubble, see:
 | 
| -    // https://github.com/Polymer/polymer/issues/292
 | 
| -    if (!event.bubbles) return;
 | 
| -
 | 
| -    bool log = _eventsLog.isLoggable(Level.FINE);
 | 
| -    if (log) {
 | 
| -      _eventsLog.fine('>>> [$localName]: hostEventListener(${event.type})');
 | 
| -    }
 | 
| -
 | 
| -    var h = findEventDelegate(event);
 | 
| -    if (h != null) {
 | 
| -      if (log) _eventsLog.fine('[$localName] found host handler name [$h]');
 | 
| -      var detail = event is CustomEvent ? event.detail : null;
 | 
| -      // TODO(jmesserly): cache the symbols?
 | 
| -      dispatchMethod(this, h, [event, detail, this]);
 | 
| -    }
 | 
| -
 | 
| -    if (log) {
 | 
| -      _eventsLog.fine('<<< [$localName]: hostEventListener(${event.type})');
 | 
| -    }
 | 
| -  }
 | 
| -
 | 
| -  String findEventDelegate(Event event) =>
 | 
| -      _declaration._eventDelegates[_eventNameFromType(event.type)];
 | 
| -
 | 
|    /// Calls [methodOrCallback] with [args] if it is a closure, otherwise, treat
 | 
|    /// it as a method name in [object], and invoke it.
 | 
|    void dispatchMethod(object, callbackOrMethod, List args) {
 | 
|      bool log = _eventsLog.isLoggable(Level.FINE);
 | 
| -    if (log) _eventsLog.fine('>>> [$localName]: dispatch $callbackOrMethod');
 | 
| +    if (log) _eventsLog.fine('>>> [$_name]: dispatch $callbackOrMethod');
 | 
|  
 | 
|      if (callbackOrMethod is Function) {
 | 
|        int maxArgs = smoke.maxArgs(callbackOrMethod);
 | 
| @@ -751,33 +854,9 @@
 | 
|        _eventsLog.warning('invalid callback');
 | 
|      }
 | 
|  
 | 
| -    if (log) _eventsLog.info('<<< [$localName]: dispatch $callbackOrMethod');
 | 
| +    if (log) _eventsLog.info('<<< [$_name]: dispatch $callbackOrMethod');
 | 
|    }
 | 
|  
 | 
| -  /// Bind events via attributes of the form `on-eventName`. This method can be
 | 
| -  /// use to hooks into the model syntax and adds event listeners as needed. By
 | 
| -  /// default, binding paths are always method names on the root model, the
 | 
| -  /// custom element in which the node exists. Adding a '@' in the path directs
 | 
| -  /// the event binding to use the model path as the event listener.  In both
 | 
| -  /// cases, the actual listener is attached to a generic method which evaluates
 | 
| -  /// the bound path at event execution time.
 | 
| -  // from src/instance/event.js#prepareBinding
 | 
| -  static PrepareBindingFunction prepareBinding(String path, String name, node) {
 | 
| -
 | 
| -    // provide an event-binding callback.
 | 
| -    return (model, node, oneTime) {
 | 
| -      if (_eventsLog.isLoggable(Level.FINE)) {
 | 
| -        _eventsLog.fine('event: [$node].$name => [$model].$path())');
 | 
| -      }
 | 
| -      var eventName = _removeEventPrefix(name);
 | 
| -      // TODO(sigmund): polymer.js dropped event translations. reconcile?
 | 
| -      var translated = _eventTranslations[eventName];
 | 
| -      eventName = translated != null ? translated : eventName;
 | 
| -
 | 
| -      return new _EventBindable(node, eventName, model, path);
 | 
| -    };
 | 
| -  }
 | 
| -
 | 
|    /// Call [methodName] method on this object with [args].
 | 
|    invokeMethod(Symbol methodName, List args) =>
 | 
|        smoke.invoke(this, methodName, args, adjust: true);
 | 
| @@ -788,13 +867,17 @@
 | 
|    ///
 | 
|    /// If you would prefer to run the callback using
 | 
|    /// [window.requestAnimationFrame], see the [async] method.
 | 
| -  // Dart note: "async" is split into 2 methods so it can have a sensible type
 | 
| -  // signatures. Also removed the various features that don't make sense in a
 | 
| -  // Dart world, like binding to "this" and taking arguments list.
 | 
| +  ///
 | 
| +  /// To cancel, call [Timer.cancel] on the result of this method.
 | 
|    Timer asyncTimer(void method(), Duration timeout) {
 | 
| +    // Dart note: "async" is split into 2 methods so it can have a sensible type
 | 
| +    // signatures. Also removed the various features that don't make sense in a
 | 
| +    // Dart world, like binding to "this" and taking arguments list.
 | 
| +
 | 
|      // when polyfilling Object.observe, ensure changes
 | 
|      // propagate before executing the async method
 | 
|      scheduleMicrotask(Observable.dirtyCheck);
 | 
| +    _Platform.callMethod('flush'); // for polymer-js interop
 | 
|      return new Timer(timeout, method);
 | 
|    }
 | 
|  
 | 
| @@ -805,24 +888,33 @@
 | 
|    ///
 | 
|    /// If you would prefer to run the callback after a given duration, see
 | 
|    /// the [asyncTimer] method.
 | 
| +  ///
 | 
| +  /// If you would like to cancel this, use [cancelAsync].
 | 
|    int async(RequestAnimationFrameCallback method) {
 | 
|      // when polyfilling Object.observe, ensure changes
 | 
|      // propagate before executing the async method
 | 
|      scheduleMicrotask(Observable.dirtyCheck);
 | 
| +    _Platform.callMethod('flush'); // for polymer-js interop
 | 
|      return window.requestAnimationFrame(method);
 | 
|    }
 | 
|  
 | 
| -  /// Fire a [CustomEvent] targeting [toNode], or this if toNode is not
 | 
| -  /// supplied. Returns the [detail] object.
 | 
| -  Object fire(String type, {Object detail, Node toNode, bool canBubble}) {
 | 
| -    var node = toNode != null ? toNode : this;
 | 
| -    //log.events && console.log('[%s]: sending [%s]', node.localName, inType);
 | 
| -    node.dispatchEvent(new CustomEvent(
 | 
| +  /// Cancel an operation scenduled by [async]. This is just shorthand for:
 | 
| +  ///     window.cancelAnimationFrame(id);
 | 
| +  void cancelAsync(int id) => window.cancelAnimationFrame(id);
 | 
| +
 | 
| +  /// Fire a [CustomEvent] targeting [onNode], or `this` if onNode is not
 | 
| +  /// supplied. Returns the new event.
 | 
| +  CustomEvent fire(String type, {Object detail, Node onNode, bool canBubble,
 | 
| +        bool cancelable}) {
 | 
| +    var node = onNode != null ? onNode : this;
 | 
| +    var event = new CustomEvent(
 | 
|        type,
 | 
|        canBubble: canBubble != null ? canBubble : true,
 | 
| +      cancelable: cancelable != null ? cancelable : true,
 | 
|        detail: detail
 | 
| -    ));
 | 
| -    return detail;
 | 
| +    );
 | 
| +    node.dispatchEvent(event);
 | 
| +    return event;
 | 
|    }
 | 
|  
 | 
|    /// Fire an event asynchronously. See [async] and [fire].
 | 
| @@ -830,7 +922,7 @@
 | 
|      // TODO(jmesserly): I'm not sure this method adds much in Dart, it's easy to
 | 
|      // add "() =>"
 | 
|      async((x) => fire(
 | 
| -        type, detail: detail, toNode: toNode, canBubble: canBubble));
 | 
| +        type, detail: detail, onNode: toNode, canBubble: canBubble));
 | 
|    }
 | 
|  
 | 
|    /// Remove [className] from [old], add class to [anew], if they exist.
 | 
| @@ -845,61 +937,93 @@
 | 
|  
 | 
|    /// Installs external stylesheets and <style> elements with the attribute
 | 
|    /// polymer-scope='controller' into the scope of element. This is intended
 | 
| -  /// to be a called during custom element construction. Note, this incurs a
 | 
| -  /// per instance cost and should be used sparingly.
 | 
| -  ///
 | 
| -  /// The need for this type of styling should go away when the shadowDOM spec
 | 
| -  /// addresses these issues:
 | 
| -  ///
 | 
| -  /// https://www.w3.org/Bugs/Public/show_bug.cgi?id=21391
 | 
| -  /// https://www.w3.org/Bugs/Public/show_bug.cgi?id=21390
 | 
| -  /// https://www.w3.org/Bugs/Public/show_bug.cgi?id=21389
 | 
| -  ///
 | 
| -  /// @param element The custom element instance into whose controller (parent)
 | 
| -  /// scope styles will be installed.
 | 
| -  /// @param elementElement The <element> containing controller styles.
 | 
| -  // TODO(sorvell): remove when spec issues are addressed
 | 
| +  /// to be called during custom element construction.
 | 
|    void installControllerStyles() {
 | 
| -    var scope = findStyleController();
 | 
| -    if (scope != null && scopeHasElementStyle(scope, _STYLE_CONTROLLER_SCOPE)) {
 | 
| +    var scope = findStyleScope();
 | 
| +    if (scope != null && !scopeHasNamedStyle(scope, localName)) {
 | 
|        // allow inherited controller styles
 | 
| -      var decl = _declaration;
 | 
| +      var decl = _element;
 | 
|        var cssText = new StringBuffer();
 | 
|        while (decl != null) {
 | 
|          cssText.write(decl.cssTextForScope(_STYLE_CONTROLLER_SCOPE));
 | 
|          decl = decl.superDeclaration;
 | 
|        }
 | 
| -      if (cssText.length > 0) {
 | 
| -        var style = decl.cssTextToScopeStyle(cssText.toString(),
 | 
| -              _STYLE_CONTROLLER_SCOPE);
 | 
| -        // TODO(sorvell): for now these styles are not shimmed
 | 
| -        // but we may need to shim them
 | 
| -        Polymer.applyStyleToScope(style, scope);
 | 
| +      if (cssText.isNotEmpty) {
 | 
| +        installScopeCssText('$cssText', scope);
 | 
|        }
 | 
|      }
 | 
|    }
 | 
|  
 | 
| -  Node findStyleController() {
 | 
| -    if (js.context.hasProperty('ShadowDOMPolyfill')) {
 | 
| -      return document.querySelector('head'); // get wrapped <head>.
 | 
| -    } else {
 | 
| -      // find the shadow root that contains this element
 | 
| -      var n = this;
 | 
| -      while (n.parentNode != null) {
 | 
| -        n = n.parentNode;
 | 
| +  void installScopeStyle(style, [String name, Node scope]) {
 | 
| +    if (scope == null) scope = findStyleScope();
 | 
| +    if (name == null) name = '';
 | 
| +
 | 
| +    if (scope != null && !scopeHasNamedStyle(scope, '$_name$name')) {
 | 
| +      var cssText = new StringBuffer();
 | 
| +      if (style is Iterable) {
 | 
| +        for (var s in style) {
 | 
| +          cssText..writeln(s.text)..writeln();
 | 
| +        }
 | 
| +      } else {
 | 
| +        cssText = (style as Node).text;
 | 
|        }
 | 
| -      return identical(n, document) ? document.head : n;
 | 
| +      installScopeCssText('$cssText', scope, name);
 | 
|      }
 | 
|    }
 | 
|  
 | 
| -  bool scopeHasElementStyle(scope, descriptor) {
 | 
| -    var rule = '$_STYLE_SCOPE_ATTRIBUTE=$localName-$descriptor';
 | 
| -    return scope.querySelector('style[$rule]') != null;
 | 
| +  void installScopeCssText(String cssText, [Node scope, String name]) {
 | 
| +    if (scope == null) scope = findStyleScope();
 | 
| +    if (name == null) name = '';
 | 
| +
 | 
| +    if (scope == null) return;
 | 
| +
 | 
| +    if (_ShadowCss != null) {
 | 
| +      cssText = _shimCssText(cssText, scope is ShadowRoot ? scope.host : null);
 | 
| +    }
 | 
| +    var style = element.cssTextToScopeStyle(cssText,
 | 
| +        _STYLE_CONTROLLER_SCOPE);
 | 
| +    applyStyleToScope(style, scope);
 | 
| +    // cache that this style has been applied
 | 
| +    Set styles = _scopeStyles[scope];
 | 
| +    if (styles == null) _scopeStyles[scope] = styles = new Set();
 | 
| +    styles.add('$_name$name');
 | 
|    }
 | 
|  
 | 
| +  Node findStyleScope([node]) {
 | 
| +    // find the shadow root that contains this element
 | 
| +    var n = node;
 | 
| +    if (n == null) n = this;
 | 
| +    while (n.parentNode != null) {
 | 
| +      n = n.parentNode;
 | 
| +    }
 | 
| +    return n;
 | 
| +  }
 | 
| +
 | 
| +  bool scopeHasNamedStyle(Node scope, String name) {
 | 
| +    Set styles = _scopeStyles[scope];
 | 
| +    return styles != null && styles.contains(name);
 | 
| +  }
 | 
| +
 | 
| +  static final _scopeStyles = new Expando();
 | 
| +
 | 
| +  static String _shimCssText(String cssText, [Element host]) {
 | 
| +    var name = '';
 | 
| +    var is_ = false;
 | 
| +    if (host != null) {
 | 
| +      name = host.localName;
 | 
| +      is_ = host.attributes.containsKey('is');
 | 
| +    }
 | 
| +    var selector = _ShadowCss.callMethod('makeScopeSelector', [name, is_]);
 | 
| +    return _ShadowCss.callMethod('shimCssText', [cssText, selector]);
 | 
| +  }
 | 
| +
 | 
|    static void applyStyleToScope(StyleElement style, Node scope) {
 | 
|      if (style == null) return;
 | 
|  
 | 
| +    if (scope == document) scope = document.head;
 | 
| +
 | 
| +    if (_hasShadowDomPolyfill) scope = document.head;
 | 
| +
 | 
|      // TODO(sorvell): necessary for IE
 | 
|      // see https://connect.microsoft.com/IE/feedback/details/790212/
 | 
|      // cloning-a-style-element-and-adding-to-document-produces
 | 
| @@ -912,8 +1036,34 @@
 | 
|        clone.attributes[_STYLE_SCOPE_ATTRIBUTE] = attr;
 | 
|      }
 | 
|  
 | 
| -    scope.append(clone);
 | 
| +    // TODO(sorvell): probably too brittle; try to figure out
 | 
| +    // where to put the element.
 | 
| +    var refNode = scope.firstChild;
 | 
| +    if (scope == document.head) {
 | 
| +      var selector = 'style[$_STYLE_SCOPE_ATTRIBUTE]';
 | 
| +      var styleElement = document.head.querySelectorAll(selector);
 | 
| +      if (styleElement.isNotEmpty) {
 | 
| +        refNode = styleElement.last.nextElementSibling;
 | 
| +      }
 | 
| +    }
 | 
| +    scope.insertBefore(clone, refNode);
 | 
|    }
 | 
| +
 | 
| +  /// Invoke [callback] in [wait], unless the job is re-registered,
 | 
| +  /// which resets the timer. If [wait] is not supplied, this will use
 | 
| +  /// [window.requestAnimationFrame] instead of a [Timer].
 | 
| +  ///
 | 
| +  /// For example:
 | 
| +  ///
 | 
| +  ///     _myJob = Polymer.scheduleJob(_myJob, callback);
 | 
| +  ///
 | 
| +  /// Returns the newly created job.
 | 
| +  // Dart note: renamed to scheduleJob to be a bit more consistent with Dart.
 | 
| +  PolymerJob scheduleJob(PolymerJob job, void callback(), [Duration wait]) {
 | 
| +    if (job == null) job = new PolymerJob._();
 | 
| +    // Dart note: made start smarter, so we don't need to call stop.
 | 
| +    return job..start(callback, wait);
 | 
| +  }
 | 
|  }
 | 
|  
 | 
|  // Dart note: Polymer addresses n-way bindings by metaprogramming: redefine
 | 
| @@ -971,80 +1121,7 @@
 | 
|  final Logger _unbindLog = new Logger('polymer.unbind');
 | 
|  final Logger _bindLog = new Logger('polymer.bind');
 | 
|  
 | 
| -final Expando _shadowHost = new Expando<Polymer>();
 | 
| -
 | 
|  final Expando _eventHandledTable = new Expando<Set<Node>>();
 | 
|  
 | 
| -/// Base class for PolymerElements deriving from HtmlElement.
 | 
| -///
 | 
| -/// See [Polymer].
 | 
| -class PolymerElement extends HtmlElement with Polymer, Observable {
 | 
| -  PolymerElement.created() : super.created() {
 | 
| -    polymerCreated();
 | 
| -  }
 | 
| -}
 | 
| +final JsObject _PolymerGestures = js.context['PolymerGestures'];
 | 
|  
 | 
| -class _PropertyValue {
 | 
| -  Object oldValue, newValue;
 | 
| -  _PropertyValue(this.oldValue);
 | 
| -}
 | 
| -
 | 
| -class PolymerExpressionsWithEvents extends PolymerExpressions {
 | 
| -  PolymerExpressionsWithEvents({Map<String, Object> globals})
 | 
| -      : super(globals: globals);
 | 
| -
 | 
| -  prepareBinding(String path, name, node) {
 | 
| -    if (_hasEventPrefix(name)) return Polymer.prepareBinding(path, name, node);
 | 
| -    return super.prepareBinding(path, name, node);
 | 
| -  }
 | 
| -}
 | 
| -
 | 
| -class _EventBindable extends Bindable {
 | 
| -  final Node _node;
 | 
| -  final String _eventName;
 | 
| -  final _model;
 | 
| -  final String _path;
 | 
| -  StreamSubscription _sub;
 | 
| -
 | 
| -  _EventBindable(this._node, this._eventName, this._model, this._path);
 | 
| -
 | 
| -  _listener(event) {
 | 
| -    var ctrlr = _findController(_node);
 | 
| -    if (ctrlr is! Polymer) return;
 | 
| -    var obj = ctrlr;
 | 
| -    var method = _path;
 | 
| -    if (_path.startsWith('@')) {
 | 
| -      obj = _model;
 | 
| -      method = new PropertyPath(_path.substring(1)).getValueFrom(_model);
 | 
| -    }
 | 
| -    var detail = event is CustomEvent ?
 | 
| -        (event as CustomEvent).detail : null;
 | 
| -    ctrlr.dispatchMethod(obj, method, [event, detail, _node]);
 | 
| -  }
 | 
| -
 | 
| -  // TODO(jmesserly): this won't find the correct host unless the ShadowRoot
 | 
| -  // was created on a PolymerElement.
 | 
| -  static Polymer _findController(Node node) {
 | 
| -    while (node.parentNode != null) {
 | 
| -      node = node.parentNode;
 | 
| -    }
 | 
| -    return _shadowHost[node];
 | 
| -  }
 | 
| -
 | 
| -  get value => null;
 | 
| -
 | 
| -  open(callback) {
 | 
| -    _sub = _node.on[_eventName].listen(_listener);
 | 
| -  }
 | 
| -
 | 
| -  close() {
 | 
| -    if (_sub != null) {
 | 
| -      if (_eventsLog.isLoggable(Level.FINE)) {
 | 
| -        _eventsLog.fine(
 | 
| -            'event.remove: [$_node].$_eventName => [$_model].$_path())');
 | 
| -      }
 | 
| -     _sub.cancel();
 | 
| -      _sub = null;
 | 
| -    }
 | 
| -  }
 | 
| -}
 | 
| 
 |