Chromium Code Reviews| Index: pkg/polymer/lib/src/instance.dart |
| diff --git a/pkg/polymer/lib/src/instance.dart b/pkg/polymer/lib/src/instance.dart |
| index c0af66c8f2cd8430245e470c9c6d6831439f8f2c..4e4d1edde8b5749420231b3f81e5e9cf371b6e67 100644 |
| --- a/pkg/polymer/lib/src/instance.dart |
| +++ b/pkg/polymer/lib/src/instance.dart |
| @@ -13,7 +13,6 @@ part of polymer; |
| * @published double volume; |
| * } |
| */ |
| -// TODO(jmesserly): does @published imply @observable or vice versa? |
| const published = const PublishedProperty(); |
| /** An annotation used to publish a field as an attribute. See [published]. */ |
| @@ -22,6 +21,37 @@ class PublishedProperty extends ObservableProperty { |
| } |
| /** |
| + * Use this type to observe a property and have the method be called when it |
| + * changes. For example: |
| + * |
| + * @ObserveProperty('foo.bar baz qux') |
|
Siggi Cherem (dart-lang)
2014/02/03 22:52:48
this is nice. getting a lot closer to what we need
Jennifer Messerly
2014/02/04 00:33:06
yeah ... I don't love it but it's an improvement.
|
| + * validate() { |
| + * // use this.foo.bar, this.baz, and this.qux in validation |
| + * ... |
| + * } |
| + * |
| + * Note that you can observe a property path, and more than a single property |
| + * can be specified in a space-delimited list or as a constant List. |
| + */ |
| +class ObserveProperty { |
| + final _names; |
| + |
| + List<String> get names { |
|
Siggi Cherem (dart-lang)
2014/02/03 22:52:48
names or paths?
|
| + var n = _names; |
| + // TODO(jmesserly): the bogus '$n' is to workaround a dart2js bug, otherwise |
| + // it generates an incorrect call site. |
|
Siggi Cherem (dart-lang)
2014/02/03 22:52:48
maybe include the bug #?
Jennifer Messerly
2014/02/04 00:33:06
I will file one, but making progress on this is mo
|
| + if (n is String) return '$n'.split(' '); |
| + if (n is! Iterable) { |
| + throw new UnsupportedError('ObserveProperty takes either an Iterable of ' |
| + 'names, or a space separated String, instead of `$n`.'); |
| + } |
| + return n; |
| + } |
| + |
| + const ObserveProperty(this._names); |
| +} |
| + |
| +/** |
| * The mixin class for Polymer elements. It provides convenience features on top |
| * of the custom elements web standard. |
| * |
| @@ -30,7 +60,7 @@ class PublishedProperty extends ObservableProperty { |
| */ |
| abstract class Polymer implements Element, Observable, NodeBindExtension { |
| // Fully ported from revision: |
| - // https://github.com/Polymer/polymer/blob/b7200854b2441a22ce89f6563963f36c50f5150d |
| + // https://github.com/Polymer/polymer/blob/37eea00e13b9f86ab21c85a955585e8e4237e3d2 |
| // |
| // src/boot.js (static APIs on "Polymer" object) |
| // src/instance/attributes.js |
| @@ -88,7 +118,7 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
| bool _unbound; // lazy-initialized |
| _Job _unbindAllJob; |
| - StreamSubscription _propertyObserver; |
| + CompoundObserver _propertyObserver; |
| bool get _elementPrepared => _declaration != null; |
| @@ -217,7 +247,7 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
| * Return a shadow-root template (if desired), override for custom behavior. |
| */ |
| Element fetchTemplate(Element elementElement) => |
| - elementElement.query('template'); |
| + elementElement.querySelector('template'); |
| /** |
| * Utility function that stamps a `<template>` into light-dom. |
| @@ -379,29 +409,27 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
| if (value is bool) { |
| return _toBoolean(value) ? '' : null; |
| - } else if (value is String || value is int || value is double) { |
| + } else if (value is String || value is num) { |
| return '$value'; |
| } |
| return null; |
| } |
| - void reflectPropertyToAttribute(Symbol name) { |
| + void reflectPropertyToAttribute(PropertyPath path) { |
| // TODO(sjmiles): consider memoizing this |
| - final self = reflect(this); |
| // try to intelligently serialize property value |
| - // TODO(jmesserly): cache symbol? |
| - final propValue = self.getField(name).reflectee; |
| + final propValue = path.getValueFrom(this); |
| final serializedValue = serializeValue(propValue); |
| // boolean properties must reflect as boolean attributes |
| if (serializedValue != null) { |
| - attributes[MirrorSystem.getName(name)] = serializedValue; |
| + attributes['$path'] = serializedValue; |
|
Siggi Cherem (dart-lang)
2014/02/03 22:52:48
is 'path' here guaranteed to be a single symbol as
Jennifer Messerly
2014/02/04 00:33:06
it is guaranteed in Polymer. added an ArgumentErro
|
| // 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(MirrorSystem.getName(name)); |
| + attributes.remove('$path'); |
| } |
| } |
| @@ -421,7 +449,9 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
| DocumentFragment instanceTemplate(Element template) => |
| templateBind(template).createInstance(this, syntax); |
| - NodeBinding bind(String name, model, [String path]) { |
| + // TODO(jmesserly): Polymer does not seem to implement the oneTime flag |
| + // correctly. File bug. |
| + 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(); |
| @@ -429,23 +459,27 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
| var property = propertyForAttribute(name); |
| if (property == null) { |
| // Cannot call super.bind because template_binding is its own package |
| - return nodeBindFallback(this).bind(name, model, path); |
| + return nodeBindFallback(this).bind(name, bindable, oneTime: oneTime); |
| } else { |
| // clean out the closets |
| unbind(name); |
| // use n-way Polymer binding |
| - var observer = bindProperty(property.simpleName, model, path); |
| + var observer = bindProperty(property.simpleName, bindable); |
| + |
| // 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 |
| - reflectPropertyToAttribute(property.simpleName); |
| + |
| + // TODO(jmesserly): polymer has the path_ in their observer object, should |
| + // we use that too instead of allocating it here? |
| + reflectPropertyToAttribute(new PropertyPath([property.simpleName])); |
| return bindings[name] = observer; |
| } |
| } |
| - Map<String, NodeBinding> get bindings => nodeBindFallback(this).bindings; |
| + Map<String, Bindable> get bindings => nodeBindFallback(this).bindings; |
| TemplateInstance get templateInstance => |
| nodeBindFallback(this).templateInstance; |
| @@ -508,59 +542,62 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
| /** Set up property observers. */ |
| void observeProperties() { |
| - // TODO(jmesserly): we don't have CompoundPathObserver, so this |
| - // implementation is a little bit different. We also don't expose the |
| - // "generateCompoundPathObserver" method. |
| final observe = _declaration._observe; |
| final publish = _declaration._publish; |
| - if (observe != null) { |
| - for (var name in observe.keys) { |
| - observeArrayValue(name, reflect(this).getField(name), null); |
| + // TODO(jmesserly): workaround for a dart2js compiler bug |
| + bool hasObserved = observe != null; |
| + |
| + if (hasObserved || publish != null) { |
| + var o = _propertyObserver = new CompoundObserver(); |
| + if (hasObserved) { |
| + for (var path in observe.keys) { |
| + 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); |
| + } |
| + } |
| + if (publish != null) { |
| + for (var path in publish.keys) { |
| + |
| + if (!hasObserved || !observe.containsKey(path)) { |
| + o.addPath(this, path); |
| + } |
| + } |
| } |
| - } |
| - if (observe != null || publish != null) { |
| - // Instead of using CompoundPathObserver, set up a binding using normal |
| - // change records. |
| - _propertyObserver = changes.listen(notifyPropertyChanges); |
| + o.open(notifyPropertyChanges); |
| } |
| } |
| + |
| /** Responds to property changes on this element. */ |
| - // Dart note: this takes a list of changes rather than trying to deal with |
| - // what CompoundPathObserver would give us. Simpler and probably faster too. |
| - void notifyPropertyChanges(Iterable<ChangeRecord> changes) { |
| + void notifyPropertyChanges(List newValues, Map oldValues, List paths) { |
| final observe = _declaration._observe; |
| final publish = _declaration._publish; |
| + final called = new HashSet(); |
| - // Summarize old and new values, so we only handle each change once. |
| - final valuePairs = new Map<Symbol, _PropertyValue>(); |
| - for (var c in changes) { |
| - if (c is! PropertyChangeRecord) continue; |
| - |
| - valuePairs.putIfAbsent(c.name, () => new _PropertyValue(c.oldValue)) |
| - .newValue = c.newValue; |
| - } |
| - |
| - valuePairs.forEach((name, pair) { |
| - if (publish != null && publish.containsKey(name)) { |
| - reflectPropertyToAttribute(name); |
| + oldValues.forEach((i, oldValue) { |
| + // note: paths is of form [object, path, object, path] |
| + var path = paths[2 * i + 1]; |
| + if (publish != null && publish.containsKey(path)) { |
| + reflectPropertyToAttribute(path); |
| } |
| if (observe == null) return; |
| - var method = observe[name]; |
| - if (method != null) { |
| + var method = observe[path]; |
| + if (method != null && called.add(method)) { |
| + final newValue = newValues[i]; |
| // observes the value if it is an array |
| - observeArrayValue(name, pair.newValue, pair.oldValue); |
| - // TODO(jmesserly): the JS code tries to avoid calling the same method |
| - // twice, but I don't see how that is possible. |
| - // Dart note: JS also passes "arguments", so we pass all change records. |
| - invokeMethod(method, [pair.oldValue, pair.newValue, changes]); |
| + observeArrayValue(path, newValue, oldValue); |
| + // Dart note: JS passes "arguments", so we pass along our args. |
| + invokeMethod(method, [oldValue, newValue, newValues, oldValues, paths]); |
| } |
| }); |
| } |
| - void observeArrayValue(Symbol name, Object value, Object old) { |
| + void observeArrayValue(PropertyPath name, Object value, Object old) { |
| final observe = _declaration._observe; |
| if (observe == null) return; |
| @@ -575,7 +612,7 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
| '$name'); |
| } |
| - unregisterObserver('${MirrorSystem.getName(name)}__array'); |
| + unregisterObserver('${name}__array'); |
| } |
| // if the new value is an array, being observing it |
| if (value is ObservableList) { |
| @@ -586,7 +623,7 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
| var sub = value.listChanges.listen((changes) { |
| invokeMethod(callbackName, [old]); |
| }); |
| - registerObserver('${MirrorSystem.getName(name)}__array', sub); |
| + registerObserver('${name}__array', sub); |
| } |
| } |
| @@ -594,7 +631,7 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
| void unbindAllProperties() { |
| if (_propertyObserver != null) { |
| - _propertyObserver.cancel(); |
| + _propertyObserver.close(); |
| _propertyObserver = null; |
| } |
| unregisterObservers(); |
| @@ -633,37 +670,28 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
| * bindProperty(#myProperty, this, 'myModel.path.to.otherProp'); |
| * } |
| */ |
| - // TODO(jmesserly): replace with something more localized, like: |
| - // @ComputedField('myModel.path.to.otherProp'); |
| - NodeBinding bindProperty(Symbol name, Object model, [String path]) => |
| - // apply Polymer two-way reference binding |
| - _bindProperties(this, name, model, path); |
| - |
| - /** |
| - * bind a property in A to a path in B by converting A[property] to a |
| - * getter/setter pair that accesses B[...path...] |
| - */ |
| - static NodeBinding _bindProperties(Polymer inA, Symbol inProperty, |
| - Object inB, String inPath) { |
| - |
| - if (_bindLog.isLoggable(Level.FINE)) { |
| - _bindLog.fine('[$inB]: bindProperties: [$inPath] to ' |
| - '[${inA.localName}].[$inProperty]'); |
| - } |
| - |
| + Bindable bindProperty(Symbol name, Bindable bindable) { |
| // 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 |
| // inside PolymerBinding. That doesn't seem unreasonable, but it's a slight |
| // difference from Polymer.js behavior. |
| + if (_bindLog.isLoggable(Level.FINE)) { |
| + _bindLog.fine('bindProperty: [$bindable] to [${localName}].[name]'); |
| + } |
| + |
| // capture A's value if B's value is null or undefined, |
| // otherwise use B's value |
| - var path = new PathObserver(inB, inPath); |
| - if (path.value == null) { |
| - path.value = reflect(inA).getField(inProperty).reflectee; |
| + // TODO(sorvell): need to review, can do with ObserverTransform |
| + var v = bindable.value; |
| + if (v == null) { |
| + bindable.value = reflect(this).getField(name).reflectee; |
| } |
| - return new _PolymerBinding(inA, inProperty, inB, inPath); |
| + |
| + // TODO(jmesserly): this will create another subscription. |
| + // It would be nice to pool this with our existing listener inside Polymer. |
|
Siggi Cherem (dart-lang)
2014/02/03 22:52:48
pool -> pull?
Jennifer Messerly
2014/02/04 00:33:06
err, I did mean "pool" as in "a pool of observers"
|
| + return new _PolymerBinding(this, name, bindable); |
| } |
| /** Attach event listeners on the host (this) element. */ |
| @@ -702,8 +730,7 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
| var h = findEventDelegate(event); |
| if (h != null) { |
| if (log) _eventsLog.fine('[$localName] found host handler name [$h]'); |
| - var detail = event is CustomEvent ? |
| - (event as CustomEvent).detail : null; |
| + var detail = event is CustomEvent ? event.detail : null; |
| // TODO(jmesserly): cache the symbols? |
| dispatchMethod(this, h, [event, detail, this]); |
| } |
| @@ -745,16 +772,10 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
| * the bound path at event execution time. |
| */ |
| // from src/instance/event.js#prepareBinding |
| - // TODO(sorvell): we're patching the syntax while evaluating |
| - // event bindings. we'll move this to a better spot when that's done |
| - static PrepareBindingFunction prepareBinding(String path, String name, node, |
| - originalPrepareBinding) { |
| - |
| - // if lhs an event prefix, |
| - if (!_hasEventPrefix(name)) return originalPrepareBinding(path, name, node); |
| + static PrepareBindingFunction prepareBinding(String path, String name, node) { |
| // provide an event-binding callback. |
| - return (model, node) { |
| + return (model, node, oneTime) { |
| if (_eventsLog.isLoggable(Level.FINE)) { |
| _eventsLog.fine('event: [$node].$name => [$model].$path())'); |
| } |
| @@ -763,46 +784,17 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
| var translated = _eventTranslations[eventName]; |
| eventName = translated != null ? translated : eventName; |
| - // TODO(jmesserly): we need a place to unregister this. See: |
| - // https://code.google.com/p/dart/issues/detail?id=15574 |
| - node.on[eventName].listen((event) { |
| - var ctrlr = _findController(node); |
| - if (ctrlr is! Polymer) return; |
| - var obj = ctrlr; |
| - var method = path; |
| - if (path[0] == '@') { |
| - obj = model; |
| - method = new PathObserver(model, path.substring(1)).value; |
| - } |
| - var detail = event is CustomEvent ? |
| - (event as CustomEvent).detail : null; |
| - ctrlr.dispatchMethod(obj, method, [event, detail, node]); |
| - }); |
| - |
| - // TODO(jmesserly): this return value is bogus. Returning null here causes |
| - // the wrong thing to happen in template_binding. |
| - return new ObservableBox(); |
| + return new _EventBindable(node, eventName, model, path); |
| }; |
| } |
| - // 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]; |
| - } |
| - |
| /** Call [methodName] method on this object with [args]. */ |
| invokeMethod(Symbol methodName, List args) => |
| _invokeMethod(this, methodName, args); |
| /** Call [methodName] method on [receiver] with [args]. */ |
| static _invokeMethod(receiver, Symbol methodName, List args) { |
| - // TODO(sigmund): consider making callbacks list all arguments |
| - // explicitly. Unless VM mirrors are optimized first, this will be expensive |
| - // once custom elements extend directly from Element (see issue 11108). |
| + // TODO(jmesserly): use function type tests instead of mirrors for dispatch. |
|
Siggi Cherem (dart-lang)
2014/02/03 22:52:48
:) exactly
Jennifer Messerly
2014/02/04 00:33:06
Done.
|
| var receiverMirror = reflect(receiver); |
| var method = _findMethod(receiverMirror.type, methodName); |
| if (method != null) { |
| @@ -999,27 +991,21 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
| // listen to changes on both sides and update the values. |
| // TODO(jmesserly): our approach leads to race conditions in the bindings. |
| // See http://code.google.com/p/dart/issues/detail?id=13567 |
| -class _PolymerBinding extends NodeBinding { |
| +class _PolymerBinding extends Bindable { |
| final InstanceMirror _target; |
| final Symbol _property; |
| + final Bindable _bindable; |
| StreamSubscription _sub; |
| Object _lastValue; |
| - _PolymerBinding(Polymer node, Symbol property, model, path) |
| - : _target = reflect(node), |
| - _property = property, |
| - super(node, MirrorSystem.getName(property), model, path) { |
| + _PolymerBinding(Polymer node, this._property, this._bindable) |
| + : _target = reflect(node) { |
| _sub = node.changes.listen(_propertyValueChanged); |
| + _updateNode(open(_updateNode)); |
| } |
| - void close() { |
| - if (closed) return; |
| - _sub.cancel(); |
| - super.close(); |
| - } |
| - |
| - void valueChanged(newValue) { |
| + void _updateNode(newValue) { |
| _lastValue = newValue; |
| _target.setField(_property, newValue); |
| } |
| @@ -1029,19 +1015,30 @@ class _PolymerBinding extends NodeBinding { |
| if (record is PropertyChangeRecord && record.name == _property) { |
| final newValue = _target.getField(_property).reflectee; |
| if (!identical(_lastValue, newValue)) { |
| - value = newValue; |
| + this.value = newValue; |
| } |
| return; |
| } |
| } |
| } |
| + |
| + open(callback(value)) => _bindable.open(callback); |
| + get value => _bindable.value; |
| + set value(newValue) => _bindable.value = newValue; |
| + |
| + void close() { |
| + if (_sub != null) { |
| + _sub.cancel(); |
| + _sub = null; |
| + } |
| + _bindable.close(); |
| + } |
| } |
| bool _toBoolean(value) => null != value && false != value; |
| TypeMirror _propertyType(DeclarationMirror property) => |
| - property is VariableMirror |
| - ? (property as VariableMirror).type |
| + property is VariableMirror ? property.type |
| : (property as MethodMirror).returnType; |
| TypeMirror _inferPropertyType(Object value, DeclarationMirror property) { |
| @@ -1095,6 +1092,58 @@ class _PropertyValue { |
| } |
| class _PolymerExpressionsWithEventDelegate extends PolymerExpressions { |
| - prepareBinding(String path, name, node) => |
| - Polymer.prepareBinding(path, name, node, super.prepareBinding); |
| + 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; |
| + } |
| + } |
| +} |