Index: pkg/polymer/lib/src/instance.dart |
diff --git a/pkg/polymer/lib/src/instance.dart b/pkg/polymer/lib/src/instance.dart |
index 3cc2e1a55a02a3e86afe83ccbbd454869e04f94f..b63ef93e49428d28ba012fa92d514d7d0a8ce9ea 100644 |
--- a/pkg/polymer/lib/src/instance.dart |
+++ b/pkg/polymer/lib/src/instance.dart |
@@ -4,7 +4,7 @@ |
part of polymer; |
-/// Use this annotation to publish a field as an attribute. |
+/// Use this annotation to publish a property as an attribute. |
/// |
/// You can also use [PublishedProperty] to provide additional information, |
/// such as automatically syncing the property back to the attribute. |
@@ -24,15 +24,31 @@ part of polymer; |
/// // |
/// // If the template is instantiated or given a model, `x` will be |
/// // used for this field and updated whenever `volume` changes. |
-/// @published double volume; |
+/// @published |
+/// double get volume => readValue(#volume); |
+/// set volume(double newValue) => writeValue(#volume, newValue); |
/// |
/// // 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; |
+/// @PublishedProperty(reflect: true) |
+/// double get volume2 => readValue(#volume2); |
+/// set volume2(double newValue) => writeValue(#volume2, newValue); |
/// } |
/// |
+/// **Important note**: the pattern using `readValue` and `writeValue` |
+/// guarantees that reading the property will give you the latest value at any |
+/// given time, even if change notifications have not been propagated. |
+/// |
+/// We still support using @published on a field, but this will not |
+/// provide the same guarantees, so this is discouraged. For example: |
+/// |
+/// // Avoid this if possible. This will be available as an HTML |
+/// // attribute too, but you might need to delay reading volume3 |
+/// // asynchronously to guarantee that you read the latest value |
+/// // set through bindings. |
+/// @published double volume3; |
const published = const PublishedProperty(); |
/// An annotation used to publish a field as an attribute. See [published]. |
@@ -72,6 +88,45 @@ class ObserveProperty { |
const ObserveProperty(this._names); |
} |
+/// Use this to create computed properties that are updated automatically. The |
+/// annotation includes a polymer expression that describes how this property |
+/// value can be expressed in terms of the values of other properties. For |
+/// example: |
+/// |
+/// class MyPlaybackElement extends PolymerElement { |
+/// @observable int x; |
+/// |
+/// // Reading xTimes2 will return x * 2. |
+/// @ComputedProperty('x * 2') |
+/// int get xTimes2 => readValue(#xTimes2); |
+/// |
+/// If the polymer expression is assignable, you can also define a setter for |
+/// it. For example: |
+/// |
+/// // Reading c will return a.b, writing c will update a.b. |
+/// @ComputedProperty('a.b') |
+/// get c => readValue(#c); |
+/// set c(newValue) => writeValue(#c, newValue); |
+/// |
+/// The expression can do anything that is allowed in a polymer expresssion, |
+/// even making calls to methods in your element. However, dependencies that are |
+/// only used within those methods and that are not visible in the polymer |
+/// expression, will not be observed. For example: |
+/// |
+/// // Because `x` only appears inside method `m`, we will not notice |
+/// // that `d` has changed if `x` is modified. However, `d` will be |
+/// // updated whenever `c` changes. |
+/// @ComputedProperty('m(c)') |
+/// get d => readValue(#d); |
+/// |
+/// m(c) => c + x; |
+class ComputedProperty { |
+ /// A polymer expression, evaluated in the context of the custom element where |
+ /// this annotation is used. |
+ final String expression; |
+ |
+ const ComputedProperty(this.expression); |
+} |
/// Base class for PolymerElements deriving from HtmlElement. |
/// |
@@ -174,7 +229,7 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
@deprecated PolymerDeclaration get declaration => _element; |
Map<String, StreamSubscription> _namedObservers; |
- List<Iterable<Bindable>> _observers = []; |
+ List<Bindable> _observers = []; |
bool _unbound; // lazy-initialized |
PolymerJob _unbindAllJob; |
@@ -238,6 +293,53 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
/// prevent that from happening. |
bool get preventDispose => false; |
+ /// Properties exposed by this element. |
+ // Dart note: unlike Javascript we can't override the original property on |
+ // the object, so we use this mechanism instead to define properties. See more |
+ // details in [_PropertyAccessor]. |
+ Map<Symbol, _PropertyAccessor> _properties = {}; |
+ |
+ /// Helper to implement a property with the given [name]. This is used for |
+ /// normal and computed properties. Normal properties can provide the initial |
+ /// value using the [initialValue] function. Computed properties ignore |
+ /// [initialValue], their value is derived from the expression in the |
+ /// [ComputedProperty] annotation that appears above the getter that uses this |
+ /// helper. |
+ readValue(Symbol name, [initialValue()]) { |
+ var property = _properties[name]; |
+ if (property == null) { |
+ var value; |
+ // Dart note: most computed properties are created in advance in |
+ // createComputedProperties, but if one computed property depends on |
+ // another, the declaration order might matter. Rather than trying to |
+ // register them in order, we include here also the option of lazily |
+ // creating the property accessor on the first read. |
+ var binding = _getBindingForComputedProperty(name); |
+ if (binding == null) { // normal property |
+ value = initialValue != null ? initialValue() : null; |
+ } else { |
+ value = binding.value; |
+ } |
+ property = _properties[name] = new _PropertyAccessor(name, this, value); |
+ } |
+ return property.value; |
+ } |
+ |
+ /// Helper to implement a setter of a property with the given [name] on a |
+ /// polymer element. This can be used on normal properties and also on |
+ /// computed properties, as long as the expression used for the computed |
+ /// property is assignable (see [ComputedProperty]). |
+ writeValue(Symbol name, newValue) { |
+ var property = _properties[name]; |
+ if (property == null) { |
+ // Note: computed properties are created in advance in |
+ // createComputedProperties, so we should only need to create here |
+ // non-computed properties. |
+ property = _properties[name] = new _PropertyAccessor(name, this, null); |
+ } |
+ property.value = newValue; |
+ } |
+ |
/// If this class is used as a mixin, this method must be called from inside |
/// of the `created()` constructor. |
/// |
@@ -291,6 +393,7 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
makeElementReady() { |
if (_readied) return; |
_readied = true; |
+ createComputedProperties(); |
// TODO(sorvell): We could create an entry point here |
// for the user to compute property values. |
@@ -554,7 +657,7 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
syntax = element.syntax; |
} |
var dom = t.createInstance(this, syntax); |
- registerObservers(getTemplateInstanceBindings(dom)); |
+ _observers.addAll(getTemplateInstanceBindings(dom)); |
return dom; |
} |
@@ -640,7 +743,7 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
if (observe != null) { |
var o = _propertyObserver = new CompoundObserver(); |
// keep track of property observer so we can shut it down |
- registerObservers([o]); |
+ _observers.add(o); |
for (var path in observe.keys) { |
o.addPath(this, path); |
@@ -656,10 +759,13 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
if (_propertyObserver != null) { |
_propertyObserver.open(notifyPropertyChanges); |
} |
- // Dart note: we need an extra listener. |
- // see comment on [_propertyChange]. |
+ |
+ // Dart note: we need an extra listener only to continue supporting |
+ // @published properties that follow the old syntax until we get rid of it. |
+ // This workaround has timing issues so we prefer the new, not so nice, |
+ // syntax. |
if (_element._publish != null) { |
- changes.listen(_propertyChange); |
+ changes.listen(_propertyChangeWorkaround); |
} |
} |
@@ -705,19 +811,26 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
} |
} |
- // 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) { |
+ // Dart note: this workaround is only for old-style @published properties, |
+ // which have timing issues. See _bindOldStylePublishedProperty below. |
+ // TODO(sigmund): deprecate this. |
+ void _propertyChangeWorkaround(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); |
- } |
+ var name = record.name; |
+ // The setter of a new-style property will create an accessor in |
+ // _properties[name]. We can skip the workaround for those properties. |
+ if (_properties[name] != null) continue; |
+ _propertyChange(name); |
+ } |
+ } |
+ |
+ void _propertyChange(Symbol nameSymbol) { |
+ var name = smoke.symbolToName(nameSymbol); |
+ var reflect = _element._reflect; |
+ if (reflect != null && reflect.contains(name)) { |
+ reflectPropertyToAttribute(name); |
} |
} |
@@ -751,19 +864,97 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
} |
} |
- void registerObservers(Iterable<Bindable> observers) { |
- _observers.add(observers); |
+ emitPropertyChangeRecord(Symbol name, newValue, oldValue) { |
+ if (identical(oldValue, newValue)) return; |
+ _propertyChange(name); |
} |
- void closeObservers() { |
- _observers.forEach(closeObserverList); |
- _observers = []; |
+ bindToAccessor(Symbol name, Bindable bindable, {resolveBindingValue: false}) { |
+ // Dart note: our pattern is to declare the initial value in the getter. We |
+ // read it via smoke to ensure that the value is initialized correctly. |
+ var oldValue = smoke.read(this, name); |
+ var property = _properties[name]; |
+ if (property == null) { |
+ // We know that _properties[name] is null only for old-style @published |
+ // properties. This fallback is here to make it easier to deprecate the |
+ // old-style of published properties, which have bad timing guarantees |
+ // (see comment in _PolymerBinding). |
+ return _bindOldStylePublishedProperty(name, bindable, oldValue); |
+ } |
+ |
+ property.bindable = bindable; |
+ var value = bindable.open(property.updateValue); |
+ |
+ if (resolveBindingValue) { |
+ // capture A's value if B's value is null or undefined, |
+ // otherwise use B's value |
+ var v = (value == null ? oldValue : value); |
+ if (!identical(value, oldValue)) { |
+ bindable.value = value = v; |
+ } |
+ } |
+ |
+ property.updateValue(value); |
+ var o = new _CloseOnlyBinding(property); |
+ _observers.add(o); |
+ return o; |
+ } |
+ |
+ // Dart note: this fallback uses our old-style binding mechanism to be able to |
+ // link @published properties with bindings. This mechanism is backwards from |
+ // what Javascript does because we can't override the original property. This |
+ // workaround also brings some timing issues which are described in detail in |
+ // dartbug.com/18343. |
+ // TODO(sigmund): deprecate old-style @published properties. |
+ _bindOldStylePublishedProperty(Symbol name, Bindable bindable, oldValue) { |
+ // capture A's value if B's value is null or undefined, |
+ // otherwise use B's value |
+ if (bindable.value == null) bindable.value = oldValue; |
+ |
+ var o = new _PolymerBinding(this, name, bindable); |
+ _observers.add(o); |
+ return o; |
+ } |
+ |
+ _getBindingForComputedProperty(Symbol name) { |
+ var exprString = element._computed[name]; |
+ if (exprString == null) return null; |
+ var expr = PolymerExpressions.getExpression(exprString); |
+ return PolymerExpressions.getBinding(expr, this, |
+ globals: element.syntax.globals); |
+ } |
+ |
+ createComputedProperties() { |
+ var computed = this.element._computed; |
+ for (var name in computed.keys) { |
+ try { |
+ // Dart note: this is done in Javascript by modifying the prototype in |
+ // declaration/properties.js, we can't do that, so we do it here. |
+ var binding = _getBindingForComputedProperty(name); |
+ |
+ // Follow up note: ideally we would only create the accessor object |
+ // here, but some computed properties might depend on others and |
+ // evaluating `binding.value` could try to read the value of another |
+ // computed property that we haven't created yet. For this reason we |
+ // also allow to also create the accessor in [readValue]. |
+ if (_properties[name] == null) { |
+ _properties[name] = new _PropertyAccessor(name, this, binding.value); |
+ } |
+ bindToAccessor(name, binding); |
+ } catch (e) { |
+ window.console.error('Failed to create computed property $name' |
+ ' (${computed[name]}): $e'); |
+ } |
+ } |
} |
- void closeObserverList(Iterable<Bindable> observers) { |
- for (var o in observers) { |
+ // Dart note: to simplify the code above we made registerObserver calls |
+ // directly invoke _observers.add/addAll. |
+ void closeObservers() { |
+ for (var o in _observers) { |
if (o != null) o.close(); |
} |
+ _observers = []; |
} |
/// Bookkeeping observers for memory management. |
@@ -800,7 +991,7 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
/// bindProperty(#myProperty, |
/// new PathObserver(this, 'myModel.path.to.otherProp')); |
/// } |
- Bindable bindProperty(Symbol name, Bindable bindable, {oneTime: false}) { |
+ Bindable bindProperty(Symbol name, bindableOrValue, {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 |
@@ -808,23 +999,20 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
// difference from Polymer.js behavior. |
if (_bindLog.isLoggable(Level.FINE)) { |
- _bindLog.fine('bindProperty: [$bindable] to [$_name].[$name]'); |
+ _bindLog.fine('bindProperty: [$bindableOrValue] to [$_name].[$name]'); |
} |
- // capture A's value if B's value is null or undefined, |
- // otherwise use B's value |
- // TODO(sorvell): need to review, can do with ObserverTransform |
- var v = bindable.value; |
- if (v == null) { |
- bindable.value = smoke.read(this, name); |
+ if (oneTime) { |
+ if (bindableOrValue is Bindable) { |
+ _bindLog.warning('bindProperty: expected non-bindable value ' |
+ 'on a one-time binding to [$_name].[$name], ' |
+ 'but found $bindableOrValue.'); |
+ } |
+ smoke.write(this, name, bindableOrValue); |
+ return null; |
} |
- // 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); |
+ return bindToAccessor(name, bindableOrValue, resolveBindingValue: true); |
} |
/// Attach event listeners on the host (this) element. |
@@ -997,9 +1185,7 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
_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'); |
+ styleCacheForScope(scope).add('$_name$name'); |
} |
Node findStyleScope([node]) { |
@@ -1012,9 +1198,23 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
return n; |
} |
- bool scopeHasNamedStyle(Node scope, String name) { |
- Set styles = _scopeStyles[scope]; |
- return styles != null && styles.contains(name); |
+ bool scopeHasNamedStyle(Node scope, String name) => |
+ styleCacheForScope(scope).contains(name); |
+ |
+ Map _polyfillScopeStyleCache = {}; |
+ |
+ Set styleCacheForScope(Node scope) { |
+ var styles; |
+ if (_hasShadowDomPolyfill) { |
+ var name = scope is ShadowRoot ? scope.host.localName |
+ : (scope as Element).localName; |
+ var styles = _polyfillScopeStyleCache[name]; |
+ if (styles == null) _polyfillScopeStyleCache[name] = styles = new Set(); |
+ } else { |
+ styles = _scopeStyles[scope]; |
+ if (styles == null) _scopeStyles[scope] = styles = new Set(); |
+ } |
+ return styles; |
} |
static final _scopeStyles = new Expando(); |
@@ -1079,12 +1279,14 @@ abstract class Polymer implements Element, Observable, NodeBindExtension { |
} |
} |
-// Dart note: Polymer addresses n-way bindings by metaprogramming: redefine |
-// the property on the PolymerElement instance to always get its value from the |
-// model@path. We can't replicate this in Dart so we do the next best thing: |
-// 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 |
+// Dart note: this is related to _bindOldStylePublishedProperty. Polymer |
+// addresses n-way bindings by metaprogramming: redefine the property on the |
+// PolymerElement instance to always get its value from the model@path. This is |
+// supported in Dart using a new style of @published property declaration using |
+// the `readValue` and `writeValue` methods above. In the past we used to work |
+// around this by listening to changes on both sides and updating the values. |
+// This object provides the hooks to do this. |
+// TODO(sigmund,jmesserly): delete after a deprecation period. |
class _PolymerBinding extends Bindable { |
final Polymer _target; |
final Symbol _property; |
@@ -1100,6 +1302,8 @@ class _PolymerBinding extends Bindable { |
void _updateNode(newValue) { |
_lastValue = newValue; |
smoke.write(_target, _property, newValue); |
+ // Note: we don't invoke emitPropertyChangeRecord here because that's |
+ // done by listening on changes on the PolymerElement. |
} |
void _propertyValueChanged(List<ChangeRecord> records) { |
@@ -1127,6 +1331,24 @@ class _PolymerBinding extends Bindable { |
} |
} |
+// Ported from an inline object in instance/properties.js#bindToAccessor. |
+class _CloseOnlyBinding extends Bindable { |
+ final _PropertyAccessor accessor; |
+ |
+ _CloseOnlyBinding(this.accessor); |
+ |
+ open(callback) {} |
+ get value => null; |
+ set value(newValue) {} |
+ deliver() {} |
+ |
+ void close() { |
+ if (accessor.bindable == null) return; |
+ accessor.bindable.close(); |
+ accessor.bindable = null; |
+ } |
+} |
+ |
bool _toBoolean(value) => null != value && false != value; |
final Logger _observeLog = new Logger('polymer.observe'); |