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; |
- } |
- } |
-} |