OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 part of polymer; | 5 part of polymer; |
6 | 6 |
7 /** | 7 /** |
8 * Use this annotation to publish a field as an attribute. For example: | 8 * Use this annotation to publish a field as an attribute. For example: |
9 * | 9 * |
10 * class MyPlaybackElement extends PolymerElement { | 10 * class MyPlaybackElement extends PolymerElement { |
11 * // This will be available as an HTML attribute, for example: | 11 * // This will be available as an HTML attribute, for example: |
12 * // <my-playback volume="11"> | 12 * // <my-playback volume="11"> |
13 * @published double volume; | 13 * @published double volume; |
14 * } | 14 * } |
15 */ | 15 */ |
16 // TODO(jmesserly): does @published imply @observable or vice versa? | |
17 const published = const PublishedProperty(); | 16 const published = const PublishedProperty(); |
18 | 17 |
19 /** An annotation used to publish a field as an attribute. See [published]. */ | 18 /** An annotation used to publish a field as an attribute. See [published]. */ |
20 class PublishedProperty extends ObservableProperty { | 19 class PublishedProperty extends ObservableProperty { |
21 const PublishedProperty(); | 20 const PublishedProperty(); |
22 } | 21 } |
23 | 22 |
24 /** | 23 /** |
| 24 * Use this type to observe a property and have the method be called when it |
| 25 * changes. For example: |
| 26 * |
| 27 * @ObserveProperty('foo.bar baz qux') |
| 28 * validate() { |
| 29 * // use this.foo.bar, this.baz, and this.qux in validation |
| 30 * ... |
| 31 * } |
| 32 * |
| 33 * Note that you can observe a property path, and more than a single property |
| 34 * can be specified in a space-delimited list or as a constant List. |
| 35 */ |
| 36 class ObserveProperty { |
| 37 final _names; |
| 38 |
| 39 List<String> get names { |
| 40 var n = _names; |
| 41 // TODO(jmesserly): the bogus '$n' is to workaround a dart2js bug, otherwise |
| 42 // it generates an incorrect call site. |
| 43 if (n is String) return '$n'.split(' '); |
| 44 if (n is! Iterable) { |
| 45 throw new UnsupportedError('ObserveProperty takes either an Iterable of ' |
| 46 'names, or a space separated String, instead of `$n`.'); |
| 47 } |
| 48 return n; |
| 49 } |
| 50 |
| 51 const ObserveProperty(this._names); |
| 52 } |
| 53 |
| 54 /** |
25 * The mixin class for Polymer elements. It provides convenience features on top | 55 * The mixin class for Polymer elements. It provides convenience features on top |
26 * of the custom elements web standard. | 56 * of the custom elements web standard. |
27 * | 57 * |
28 * If this class is used as a mixin, | 58 * If this class is used as a mixin, |
29 * you must call `polymerCreated()` from the body of your constructor. | 59 * you must call `polymerCreated()` from the body of your constructor. |
30 */ | 60 */ |
31 abstract class Polymer implements Element, Observable, NodeBindExtension { | 61 abstract class Polymer implements Element, Observable, NodeBindExtension { |
32 // Fully ported from revision: | 62 // Fully ported from revision: |
33 // https://github.com/Polymer/polymer/blob/b7200854b2441a22ce89f6563963f36c50f
5150d | 63 // https://github.com/Polymer/polymer/blob/37eea00e13b9f86ab21c85a955585e8e423
7e3d2 |
34 // | 64 // |
35 // src/boot.js (static APIs on "Polymer" object) | 65 // src/boot.js (static APIs on "Polymer" object) |
36 // src/instance/attributes.js | 66 // src/instance/attributes.js |
37 // src/instance/base.js | 67 // src/instance/base.js |
38 // src/instance/events.js | 68 // src/instance/events.js |
39 // src/instance/mdv.js | 69 // src/instance/mdv.js |
40 // src/instance/properties.js | 70 // src/instance/properties.js |
41 // src/instance/style.js | 71 // src/instance/style.js |
42 // src/instance/utils.js | 72 // src/instance/utils.js |
43 | 73 |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
81 | 111 |
82 PolymerDeclaration _declaration; | 112 PolymerDeclaration _declaration; |
83 | 113 |
84 /** The most derived `<polymer-element>` declaration for this element. */ | 114 /** The most derived `<polymer-element>` declaration for this element. */ |
85 PolymerDeclaration get declaration => _declaration; | 115 PolymerDeclaration get declaration => _declaration; |
86 | 116 |
87 Map<String, StreamSubscription> _observers; | 117 Map<String, StreamSubscription> _observers; |
88 bool _unbound; // lazy-initialized | 118 bool _unbound; // lazy-initialized |
89 _Job _unbindAllJob; | 119 _Job _unbindAllJob; |
90 | 120 |
91 StreamSubscription _propertyObserver; | 121 CompoundObserver _propertyObserver; |
92 | 122 |
93 bool get _elementPrepared => _declaration != null; | 123 bool get _elementPrepared => _declaration != null; |
94 | 124 |
95 bool get applyAuthorStyles => false; | 125 bool get applyAuthorStyles => false; |
96 bool get resetStyleInheritance => false; | 126 bool get resetStyleInheritance => false; |
97 bool get alwaysPrepare => false; | 127 bool get alwaysPrepare => false; |
98 bool get preventDispose => false; | 128 bool get preventDispose => false; |
99 | 129 |
100 BindingDelegate syntax = _polymerSyntax; | 130 BindingDelegate syntax = _polymerSyntax; |
101 | 131 |
(...skipping 108 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
210 | 240 |
211 var name = elementElement.attributes['name']; | 241 var name = elementElement.attributes['name']; |
212 if (name == null) return; | 242 if (name == null) return; |
213 _shadowRoots[name] = root; | 243 _shadowRoots[name] = root; |
214 } | 244 } |
215 | 245 |
216 /** | 246 /** |
217 * Return a shadow-root template (if desired), override for custom behavior. | 247 * Return a shadow-root template (if desired), override for custom behavior. |
218 */ | 248 */ |
219 Element fetchTemplate(Element elementElement) => | 249 Element fetchTemplate(Element elementElement) => |
220 elementElement.query('template'); | 250 elementElement.querySelector('template'); |
221 | 251 |
222 /** | 252 /** |
223 * Utility function that stamps a `<template>` into light-dom. | 253 * Utility function that stamps a `<template>` into light-dom. |
224 */ | 254 */ |
225 Node lightFromTemplate(Element template) { | 255 Node lightFromTemplate(Element template) { |
226 if (template == null) return null; | 256 if (template == null) return null; |
227 // stamp template | 257 // stamp template |
228 // which includes parsing and applying MDV bindings before being | 258 // which includes parsing and applying MDV bindings before being |
229 // inserted (to avoid {{}} in attribute values) | 259 // inserted (to avoid {{}} in attribute values) |
230 // e.g. to prevent <img src="images/{{icon}}"> from generating a 404. | 260 // e.g. to prevent <img src="images/{{icon}}"> from generating a 404. |
(...skipping 141 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
372 // TypeMirror, but it is currently impossible to get from a TypeMirror to a | 402 // TypeMirror, but it is currently impossible to get from a TypeMirror to a |
373 // ClassMirror. | 403 // ClassMirror. |
374 Object deserializeValue(String value, Object currentValue, TypeMirror type) => | 404 Object deserializeValue(String value, Object currentValue, TypeMirror type) => |
375 deserialize.deserializeValue(value, currentValue, type); | 405 deserialize.deserializeValue(value, currentValue, type); |
376 | 406 |
377 String serializeValue(Object value) { | 407 String serializeValue(Object value) { |
378 if (value == null) return null; | 408 if (value == null) return null; |
379 | 409 |
380 if (value is bool) { | 410 if (value is bool) { |
381 return _toBoolean(value) ? '' : null; | 411 return _toBoolean(value) ? '' : null; |
382 } else if (value is String || value is int || value is double) { | 412 } else if (value is String || value is num) { |
383 return '$value'; | 413 return '$value'; |
384 } | 414 } |
385 return null; | 415 return null; |
386 } | 416 } |
387 | 417 |
388 void reflectPropertyToAttribute(Symbol name) { | 418 void reflectPropertyToAttribute(PropertyPath path) { |
| 419 if (path.length != 1) throw new ArgumentError('path must be length 1'); |
| 420 |
389 // TODO(sjmiles): consider memoizing this | 421 // TODO(sjmiles): consider memoizing this |
390 final self = reflect(this); | |
391 // try to intelligently serialize property value | 422 // try to intelligently serialize property value |
392 // TODO(jmesserly): cache symbol? | 423 final propValue = path.getValueFrom(this); |
393 final propValue = self.getField(name).reflectee; | |
394 final serializedValue = serializeValue(propValue); | 424 final serializedValue = serializeValue(propValue); |
395 // boolean properties must reflect as boolean attributes | 425 // boolean properties must reflect as boolean attributes |
396 if (serializedValue != null) { | 426 if (serializedValue != null) { |
397 attributes[MirrorSystem.getName(name)] = serializedValue; | 427 attributes['$path'] = serializedValue; |
398 // TODO(sorvell): we should remove attr for all properties | 428 // TODO(sorvell): we should remove attr for all properties |
399 // that have undefined serialization; however, we will need to | 429 // that have undefined serialization; however, we will need to |
400 // refine the attr reflection system to achieve this; pica, for example, | 430 // refine the attr reflection system to achieve this; pica, for example, |
401 // relies on having inferredType object properties not removed as | 431 // relies on having inferredType object properties not removed as |
402 // attrs. | 432 // attrs. |
403 } else if (propValue is bool) { | 433 } else if (propValue is bool) { |
404 attributes.remove(MirrorSystem.getName(name)); | 434 attributes.remove('$path'); |
405 } | 435 } |
406 } | 436 } |
407 | 437 |
408 /** | 438 /** |
409 * Creates the document fragment to use for each instance of the custom | 439 * Creates the document fragment to use for each instance of the custom |
410 * element, given the `<template>` node. By default this is equivalent to: | 440 * element, given the `<template>` node. By default this is equivalent to: |
411 * | 441 * |
412 * templateBind(template).createInstance(this, polymerSyntax); | 442 * templateBind(template).createInstance(this, polymerSyntax); |
413 * | 443 * |
414 * Where polymerSyntax is a singleton `PolymerExpressions` instance from the | 444 * Where polymerSyntax is a singleton `PolymerExpressions` instance from the |
415 * [polymer_expressions](https://pub.dartlang.org/packages/polymer_expressions
) | 445 * [polymer_expressions](https://pub.dartlang.org/packages/polymer_expressions
) |
416 * package. | 446 * package. |
417 * | 447 * |
418 * You can override this method to change the instantiation behavior of the | 448 * You can override this method to change the instantiation behavior of the |
419 * template, for example to use a different data-binding syntax. | 449 * template, for example to use a different data-binding syntax. |
420 */ | 450 */ |
421 DocumentFragment instanceTemplate(Element template) => | 451 DocumentFragment instanceTemplate(Element template) => |
422 templateBind(template).createInstance(this, syntax); | 452 templateBind(template).createInstance(this, syntax); |
423 | 453 |
424 NodeBinding bind(String name, model, [String path]) { | 454 // TODO(jmesserly): Polymer does not seem to implement the oneTime flag |
| 455 // correctly. File bug. |
| 456 Bindable bind(String name, Bindable bindable, {bool oneTime: false}) { |
425 // note: binding is a prepare signal. This allows us to be sure that any | 457 // note: binding is a prepare signal. This allows us to be sure that any |
426 // property changes that occur as a result of binding will be observed. | 458 // property changes that occur as a result of binding will be observed. |
427 if (!_elementPrepared) prepareElement(); | 459 if (!_elementPrepared) prepareElement(); |
428 | 460 |
429 var property = propertyForAttribute(name); | 461 var property = propertyForAttribute(name); |
430 if (property == null) { | 462 if (property == null) { |
431 // Cannot call super.bind because template_binding is its own package | 463 // Cannot call super.bind because template_binding is its own package |
432 return nodeBindFallback(this).bind(name, model, path); | 464 return nodeBindFallback(this).bind(name, bindable, oneTime: oneTime); |
433 } else { | 465 } else { |
434 // clean out the closets | 466 // clean out the closets |
435 unbind(name); | 467 unbind(name); |
436 // use n-way Polymer binding | 468 // use n-way Polymer binding |
437 var observer = bindProperty(property.simpleName, model, path); | 469 var observer = bindProperty(property.simpleName, bindable); |
| 470 |
438 // reflect bound property to attribute when binding | 471 // reflect bound property to attribute when binding |
439 // to ensure binding is not left on attribute if property | 472 // to ensure binding is not left on attribute if property |
440 // does not update due to not changing. | 473 // does not update due to not changing. |
441 // Dart note: we include this patch: | 474 // Dart note: we include this patch: |
442 // https://github.com/Polymer/polymer/pull/319 | 475 // https://github.com/Polymer/polymer/pull/319 |
443 reflectPropertyToAttribute(property.simpleName); | 476 |
| 477 // TODO(jmesserly): polymer has the path_ in their observer object, should |
| 478 // we use that too instead of allocating it here? |
| 479 reflectPropertyToAttribute(new PropertyPath([property.simpleName])); |
444 return bindings[name] = observer; | 480 return bindings[name] = observer; |
445 } | 481 } |
446 } | 482 } |
447 | 483 |
448 Map<String, NodeBinding> get bindings => nodeBindFallback(this).bindings; | 484 Map<String, Bindable> get bindings => nodeBindFallback(this).bindings; |
449 TemplateInstance get templateInstance => | 485 TemplateInstance get templateInstance => |
450 nodeBindFallback(this).templateInstance; | 486 nodeBindFallback(this).templateInstance; |
451 | 487 |
452 void unbind(String name) => nodeBindFallback(this).unbind(name); | 488 void unbind(String name) => nodeBindFallback(this).unbind(name); |
453 | 489 |
454 void asyncUnbindAll() { | 490 void asyncUnbindAll() { |
455 if (_unbound == true) return; | 491 if (_unbound == true) return; |
456 _unbindLog.fine('[$localName] asyncUnbindAll'); | 492 _unbindLog.fine('[$localName] asyncUnbindAll'); |
457 _unbindAllJob = _runJob(_unbindAllJob, unbindAll, Duration.ZERO); | 493 _unbindAllJob = _runJob(_unbindAllJob, unbindAll, Duration.ZERO); |
458 } | 494 } |
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
501 if (node == null) return; | 537 if (node == null) return; |
502 | 538 |
503 callback(node); | 539 callback(node); |
504 for (var child = node.firstChild; child != null; child = child.nextNode) { | 540 for (var child = node.firstChild; child != null; child = child.nextNode) { |
505 _forNodeTree(child, callback); | 541 _forNodeTree(child, callback); |
506 } | 542 } |
507 } | 543 } |
508 | 544 |
509 /** Set up property observers. */ | 545 /** Set up property observers. */ |
510 void observeProperties() { | 546 void observeProperties() { |
511 // TODO(jmesserly): we don't have CompoundPathObserver, so this | |
512 // implementation is a little bit different. We also don't expose the | |
513 // "generateCompoundPathObserver" method. | |
514 final observe = _declaration._observe; | 547 final observe = _declaration._observe; |
515 final publish = _declaration._publish; | 548 final publish = _declaration._publish; |
516 | 549 |
517 if (observe != null) { | 550 // TODO(jmesserly): workaround for a dart2js compiler bug |
518 for (var name in observe.keys) { | 551 bool hasObserved = observe != null; |
519 observeArrayValue(name, reflect(this).getField(name), null); | 552 |
| 553 if (hasObserved || publish != null) { |
| 554 var o = _propertyObserver = new CompoundObserver(); |
| 555 if (hasObserved) { |
| 556 for (var path in observe.keys) { |
| 557 o.addPath(this, path); |
| 558 |
| 559 // TODO(jmesserly): on the Polymer side it doesn't look like they |
| 560 // will observe arrays unless it is a length == 1 path. |
| 561 observeArrayValue(path, path.getValueFrom(this), null); |
| 562 } |
520 } | 563 } |
521 } | 564 if (publish != null) { |
522 if (observe != null || publish != null) { | 565 for (var path in publish.keys) { |
523 // Instead of using CompoundPathObserver, set up a binding using normal | 566 |
524 // change records. | 567 if (!hasObserved || !observe.containsKey(path)) { |
525 _propertyObserver = changes.listen(notifyPropertyChanges); | 568 o.addPath(this, path); |
| 569 } |
| 570 } |
| 571 } |
| 572 o.open(notifyPropertyChanges); |
526 } | 573 } |
527 } | 574 } |
528 | 575 |
| 576 |
529 /** Responds to property changes on this element. */ | 577 /** Responds to property changes on this element. */ |
530 // Dart note: this takes a list of changes rather than trying to deal with | 578 void notifyPropertyChanges(List newValues, Map oldValues, List paths) { |
531 // what CompoundPathObserver would give us. Simpler and probably faster too. | |
532 void notifyPropertyChanges(Iterable<ChangeRecord> changes) { | |
533 final observe = _declaration._observe; | 579 final observe = _declaration._observe; |
534 final publish = _declaration._publish; | 580 final publish = _declaration._publish; |
| 581 final called = new HashSet(); |
535 | 582 |
536 // Summarize old and new values, so we only handle each change once. | 583 oldValues.forEach((i, oldValue) { |
537 final valuePairs = new Map<Symbol, _PropertyValue>(); | 584 // note: paths is of form [object, path, object, path] |
538 for (var c in changes) { | 585 var path = paths[2 * i + 1]; |
539 if (c is! PropertyChangeRecord) continue; | 586 if (publish != null && publish.containsKey(path)) { |
540 | 587 reflectPropertyToAttribute(path); |
541 valuePairs.putIfAbsent(c.name, () => new _PropertyValue(c.oldValue)) | |
542 .newValue = c.newValue; | |
543 } | |
544 | |
545 valuePairs.forEach((name, pair) { | |
546 if (publish != null && publish.containsKey(name)) { | |
547 reflectPropertyToAttribute(name); | |
548 } | 588 } |
549 if (observe == null) return; | 589 if (observe == null) return; |
550 | 590 |
551 var method = observe[name]; | 591 var methods = observe[path]; |
552 if (method != null) { | 592 if (methods == null) return; |
| 593 |
| 594 for (var method in methods) { |
| 595 if (!called.add(method)) continue; // don't invoke more than once. |
| 596 |
| 597 final newValue = newValues[i]; |
553 // observes the value if it is an array | 598 // observes the value if it is an array |
554 observeArrayValue(name, pair.newValue, pair.oldValue); | 599 observeArrayValue(path, newValue, oldValue); |
555 // TODO(jmesserly): the JS code tries to avoid calling the same method | 600 // Dart note: JS passes "arguments", so we pass along our args. |
556 // twice, but I don't see how that is possible. | 601 invokeMethod(method, [oldValue, newValue, newValues, oldValues, paths]); |
557 // Dart note: JS also passes "arguments", so we pass all change records. | |
558 invokeMethod(method, [pair.oldValue, pair.newValue, changes]); | |
559 } | 602 } |
560 }); | 603 }); |
561 } | 604 } |
562 | 605 |
563 void observeArrayValue(Symbol name, Object value, Object old) { | 606 void observeArrayValue(PropertyPath name, Object value, Object old) { |
564 final observe = _declaration._observe; | 607 final observe = _declaration._observe; |
565 if (observe == null) return; | 608 if (observe == null) return; |
566 | 609 |
567 // we only care if there are registered side-effects | 610 // we only care if there are registered side-effects |
568 var callbackName = observe[name]; | 611 var callbacks = observe[name]; |
569 if (callbackName == null) return; | 612 if (callbacks == null) return; |
570 | 613 |
571 // if we are observing the previous value, stop | 614 // if we are observing the previous value, stop |
572 if (old is ObservableList) { | 615 if (old is ObservableList) { |
573 if (_observeLog.isLoggable(Level.FINE)) { | 616 if (_observeLog.isLoggable(Level.FINE)) { |
574 _observeLog.fine('[$localName] observeArrayValue: unregister observer ' | 617 _observeLog.fine('[$localName] observeArrayValue: unregister observer ' |
575 '$name'); | 618 '$name'); |
576 } | 619 } |
577 | 620 |
578 unregisterObserver('${MirrorSystem.getName(name)}__array'); | 621 unregisterObserver('${name}__array'); |
579 } | 622 } |
580 // if the new value is an array, being observing it | 623 // if the new value is an array, being observing it |
581 if (value is ObservableList) { | 624 if (value is ObservableList) { |
582 if (_observeLog.isLoggable(Level.FINE)) { | 625 if (_observeLog.isLoggable(Level.FINE)) { |
583 _observeLog.fine('[$localName] observeArrayValue: register observer ' | 626 _observeLog.fine('[$localName] observeArrayValue: register observer ' |
584 '$name'); | 627 '$name'); |
585 } | 628 } |
586 var sub = value.listChanges.listen((changes) { | 629 var sub = value.listChanges.listen((changes) { |
587 invokeMethod(callbackName, [old]); | 630 for (var callback in callbacks) { |
| 631 invokeMethod(callback, [old]); |
| 632 } |
588 }); | 633 }); |
589 registerObserver('${MirrorSystem.getName(name)}__array', sub); | 634 registerObserver('${name}__array', sub); |
590 } | 635 } |
591 } | 636 } |
592 | 637 |
593 bool unbindProperty(String name) => unregisterObserver(name); | 638 bool unbindProperty(String name) => unregisterObserver(name); |
594 | 639 |
595 void unbindAllProperties() { | 640 void unbindAllProperties() { |
596 if (_propertyObserver != null) { | 641 if (_propertyObserver != null) { |
597 _propertyObserver.cancel(); | 642 _propertyObserver.close(); |
598 _propertyObserver = null; | 643 _propertyObserver = null; |
599 } | 644 } |
600 unregisterObservers(); | 645 unregisterObservers(); |
601 } | 646 } |
602 | 647 |
603 /** Bookkeeping observers for memory management. */ | 648 /** Bookkeeping observers for memory management. */ |
604 void registerObserver(String name, StreamSubscription sub) { | 649 void registerObserver(String name, StreamSubscription sub) { |
605 if (_observers == null) { | 650 if (_observers == null) { |
606 _observers = new Map<String, StreamSubscription>(); | 651 _observers = new Map<String, StreamSubscription>(); |
607 } | 652 } |
(...skipping 18 matching lines...) Expand all Loading... |
626 * Bind a [property] in this object to a [path] in model. *Note* in Dart it | 671 * Bind a [property] in this object to a [path] in model. *Note* in Dart it |
627 * is necessary to also define the field: | 672 * is necessary to also define the field: |
628 * | 673 * |
629 * var myProperty; | 674 * var myProperty; |
630 * | 675 * |
631 * ready() { | 676 * ready() { |
632 * super.ready(); | 677 * super.ready(); |
633 * bindProperty(#myProperty, this, 'myModel.path.to.otherProp'); | 678 * bindProperty(#myProperty, this, 'myModel.path.to.otherProp'); |
634 * } | 679 * } |
635 */ | 680 */ |
636 // TODO(jmesserly): replace with something more localized, like: | 681 Bindable bindProperty(Symbol name, Bindable bindable) { |
637 // @ComputedField('myModel.path.to.otherProp'); | |
638 NodeBinding bindProperty(Symbol name, Object model, [String path]) => | |
639 // apply Polymer two-way reference binding | |
640 _bindProperties(this, name, model, path); | |
641 | |
642 /** | |
643 * bind a property in A to a path in B by converting A[property] to a | |
644 * getter/setter pair that accesses B[...path...] | |
645 */ | |
646 static NodeBinding _bindProperties(Polymer inA, Symbol inProperty, | |
647 Object inB, String inPath) { | |
648 | |
649 if (_bindLog.isLoggable(Level.FINE)) { | |
650 _bindLog.fine('[$inB]: bindProperties: [$inPath] to ' | |
651 '[${inA.localName}].[$inProperty]'); | |
652 } | |
653 | |
654 // Dart note: normally we only reach this code when we know it's a | 682 // Dart note: normally we only reach this code when we know it's a |
655 // property, but if someone uses bindProperty directly they might get a | 683 // property, but if someone uses bindProperty directly they might get a |
656 // NoSuchMethodError either from the getField below, or from the setField | 684 // NoSuchMethodError either from the getField below, or from the setField |
657 // inside PolymerBinding. That doesn't seem unreasonable, but it's a slight | 685 // inside PolymerBinding. That doesn't seem unreasonable, but it's a slight |
658 // difference from Polymer.js behavior. | 686 // difference from Polymer.js behavior. |
659 | 687 |
| 688 if (_bindLog.isLoggable(Level.FINE)) { |
| 689 _bindLog.fine('bindProperty: [$bindable] to [${localName}].[name]'); |
| 690 } |
| 691 |
660 // capture A's value if B's value is null or undefined, | 692 // capture A's value if B's value is null or undefined, |
661 // otherwise use B's value | 693 // otherwise use B's value |
662 var path = new PathObserver(inB, inPath); | 694 // TODO(sorvell): need to review, can do with ObserverTransform |
663 if (path.value == null) { | 695 var v = bindable.value; |
664 path.value = reflect(inA).getField(inProperty).reflectee; | 696 if (v == null) { |
| 697 bindable.value = reflect(this).getField(name).reflectee; |
665 } | 698 } |
666 return new _PolymerBinding(inA, inProperty, inB, inPath); | 699 |
| 700 // TODO(jmesserly): this will create another subscription. |
| 701 // It would be nice to have this reuse our existing _propertyObserver |
| 702 // created by observeProperties, to avoid more observation overhead. |
| 703 return new _PolymerBinding(this, name, bindable); |
667 } | 704 } |
668 | 705 |
669 /** Attach event listeners on the host (this) element. */ | 706 /** Attach event listeners on the host (this) element. */ |
670 void addHostListeners() { | 707 void addHostListeners() { |
671 var events = _declaration._eventDelegates; | 708 var events = _declaration._eventDelegates; |
672 if (events.isEmpty) return; | 709 if (events.isEmpty) return; |
673 | 710 |
674 if (_eventsLog.isLoggable(Level.FINE)) { | 711 if (_eventsLog.isLoggable(Level.FINE)) { |
675 _eventsLog.fine('[$localName] addHostListeners: $events'); | 712 _eventsLog.fine('[$localName] addHostListeners: $events'); |
676 } | 713 } |
(...skipping 18 matching lines...) Expand all Loading... |
695 if (!event.bubbles) return; | 732 if (!event.bubbles) return; |
696 | 733 |
697 bool log = _eventsLog.isLoggable(Level.FINE); | 734 bool log = _eventsLog.isLoggable(Level.FINE); |
698 if (log) { | 735 if (log) { |
699 _eventsLog.fine('>>> [$localName]: hostEventListener(${event.type})'); | 736 _eventsLog.fine('>>> [$localName]: hostEventListener(${event.type})'); |
700 } | 737 } |
701 | 738 |
702 var h = findEventDelegate(event); | 739 var h = findEventDelegate(event); |
703 if (h != null) { | 740 if (h != null) { |
704 if (log) _eventsLog.fine('[$localName] found host handler name [$h]'); | 741 if (log) _eventsLog.fine('[$localName] found host handler name [$h]'); |
705 var detail = event is CustomEvent ? | 742 var detail = event is CustomEvent ? event.detail : null; |
706 (event as CustomEvent).detail : null; | |
707 // TODO(jmesserly): cache the symbols? | 743 // TODO(jmesserly): cache the symbols? |
708 dispatchMethod(this, h, [event, detail, this]); | 744 dispatchMethod(this, h, [event, detail, this]); |
709 } | 745 } |
710 | 746 |
711 if (log) { | 747 if (log) { |
712 _eventsLog.fine('<<< [$localName]: hostEventListener(${event.type})'); | 748 _eventsLog.fine('<<< [$localName]: hostEventListener(${event.type})'); |
713 } | 749 } |
714 } | 750 } |
715 | 751 |
716 String findEventDelegate(Event event) => | 752 String findEventDelegate(Event event) => |
(...skipping 21 matching lines...) Expand all Loading... |
738 /** | 774 /** |
739 * Bind events via attributes of the form `on-eventName`. This method can be | 775 * Bind events via attributes of the form `on-eventName`. This method can be |
740 * use to hooks into the model syntax and adds event listeners as needed. By | 776 * use to hooks into the model syntax and adds event listeners as needed. By |
741 * default, binding paths are always method names on the root model, the | 777 * default, binding paths are always method names on the root model, the |
742 * custom element in which the node exists. Adding a '@' in the path directs | 778 * custom element in which the node exists. Adding a '@' in the path directs |
743 * the event binding to use the model path as the event listener. In both | 779 * the event binding to use the model path as the event listener. In both |
744 * cases, the actual listener is attached to a generic method which evaluates | 780 * cases, the actual listener is attached to a generic method which evaluates |
745 * the bound path at event execution time. | 781 * the bound path at event execution time. |
746 */ | 782 */ |
747 // from src/instance/event.js#prepareBinding | 783 // from src/instance/event.js#prepareBinding |
748 // TODO(sorvell): we're patching the syntax while evaluating | 784 static PrepareBindingFunction prepareBinding(String path, String name, node) { |
749 // event bindings. we'll move this to a better spot when that's done | |
750 static PrepareBindingFunction prepareBinding(String path, String name, node, | |
751 originalPrepareBinding) { | |
752 | |
753 // if lhs an event prefix, | |
754 if (!_hasEventPrefix(name)) return originalPrepareBinding(path, name, node); | |
755 | 785 |
756 // provide an event-binding callback. | 786 // provide an event-binding callback. |
757 return (model, node) { | 787 return (model, node, oneTime) { |
758 if (_eventsLog.isLoggable(Level.FINE)) { | 788 if (_eventsLog.isLoggable(Level.FINE)) { |
759 _eventsLog.fine('event: [$node].$name => [$model].$path())'); | 789 _eventsLog.fine('event: [$node].$name => [$model].$path())'); |
760 } | 790 } |
761 var eventName = _removeEventPrefix(name); | 791 var eventName = _removeEventPrefix(name); |
762 // TODO(sigmund): polymer.js dropped event translations. reconcile? | 792 // TODO(sigmund): polymer.js dropped event translations. reconcile? |
763 var translated = _eventTranslations[eventName]; | 793 var translated = _eventTranslations[eventName]; |
764 eventName = translated != null ? translated : eventName; | 794 eventName = translated != null ? translated : eventName; |
765 | 795 |
766 // TODO(jmesserly): we need a place to unregister this. See: | 796 return new _EventBindable(node, eventName, model, path); |
767 // https://code.google.com/p/dart/issues/detail?id=15574 | |
768 node.on[eventName].listen((event) { | |
769 var ctrlr = _findController(node); | |
770 if (ctrlr is! Polymer) return; | |
771 var obj = ctrlr; | |
772 var method = path; | |
773 if (path[0] == '@') { | |
774 obj = model; | |
775 method = new PathObserver(model, path.substring(1)).value; | |
776 } | |
777 var detail = event is CustomEvent ? | |
778 (event as CustomEvent).detail : null; | |
779 ctrlr.dispatchMethod(obj, method, [event, detail, node]); | |
780 }); | |
781 | |
782 // TODO(jmesserly): this return value is bogus. Returning null here causes | |
783 // the wrong thing to happen in template_binding. | |
784 return new ObservableBox(); | |
785 }; | 797 }; |
786 } | 798 } |
787 | 799 |
788 // TODO(jmesserly): this won't find the correct host unless the ShadowRoot | |
789 // was created on a PolymerElement. | |
790 static Polymer _findController(Node node) { | |
791 while (node.parentNode != null) { | |
792 node = node.parentNode; | |
793 } | |
794 return _shadowHost[node]; | |
795 } | |
796 | |
797 /** Call [methodName] method on this object with [args]. */ | 800 /** Call [methodName] method on this object with [args]. */ |
798 invokeMethod(Symbol methodName, List args) => | 801 invokeMethod(Symbol methodName, List args) => |
799 _invokeMethod(this, methodName, args); | 802 _invokeMethod(this, methodName, args); |
800 | 803 |
801 /** Call [methodName] method on [receiver] with [args]. */ | 804 /** Call [methodName] method on [receiver] with [args]. */ |
802 static _invokeMethod(receiver, Symbol methodName, List args) { | 805 static _invokeMethod(receiver, Symbol methodName, List args) { |
803 // TODO(sigmund): consider making callbacks list all arguments | 806 // TODO(jmesserly): use function type tests instead of mirrors for dispatch. |
804 // explicitly. Unless VM mirrors are optimized first, this will be expensive | |
805 // once custom elements extend directly from Element (see issue 11108). | |
806 var receiverMirror = reflect(receiver); | 807 var receiverMirror = reflect(receiver); |
807 var method = _findMethod(receiverMirror.type, methodName); | 808 var method = _findMethod(receiverMirror.type, methodName); |
808 if (method != null) { | 809 if (method != null) { |
809 // This will either truncate the argument list or extend it with extra | 810 // This will either truncate the argument list or extend it with extra |
810 // null arguments, so it will match the signature. | 811 // null arguments, so it will match the signature. |
811 // TODO(sigmund): consider accepting optional arguments when we can tell | 812 // TODO(sigmund): consider accepting optional arguments when we can tell |
812 // them appart from named arguments (see http://dartbug.com/11334) | 813 // them appart from named arguments (see http://dartbug.com/11334) |
813 args.length = method.parameters.where((p) => !p.isOptional).length; | 814 args.length = method.parameters.where((p) => !p.isOptional).length; |
814 } | 815 } |
815 return receiverMirror.invoke(methodName, args).reflectee; | 816 return receiverMirror.invoke(methodName, args).reflectee; |
(...skipping 176 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
992 }); | 993 }); |
993 } | 994 } |
994 } | 995 } |
995 | 996 |
996 // Dart note: Polymer addresses n-way bindings by metaprogramming: redefine | 997 // Dart note: Polymer addresses n-way bindings by metaprogramming: redefine |
997 // the property on the PolymerElement instance to always get its value from the | 998 // the property on the PolymerElement instance to always get its value from the |
998 // model@path. We can't replicate this in Dart so we do the next best thing: | 999 // model@path. We can't replicate this in Dart so we do the next best thing: |
999 // listen to changes on both sides and update the values. | 1000 // listen to changes on both sides and update the values. |
1000 // TODO(jmesserly): our approach leads to race conditions in the bindings. | 1001 // TODO(jmesserly): our approach leads to race conditions in the bindings. |
1001 // See http://code.google.com/p/dart/issues/detail?id=13567 | 1002 // See http://code.google.com/p/dart/issues/detail?id=13567 |
1002 class _PolymerBinding extends NodeBinding { | 1003 class _PolymerBinding extends Bindable { |
1003 final InstanceMirror _target; | 1004 final InstanceMirror _target; |
1004 final Symbol _property; | 1005 final Symbol _property; |
| 1006 final Bindable _bindable; |
1005 StreamSubscription _sub; | 1007 StreamSubscription _sub; |
1006 Object _lastValue; | 1008 Object _lastValue; |
1007 | 1009 |
1008 _PolymerBinding(Polymer node, Symbol property, model, path) | 1010 _PolymerBinding(Polymer node, this._property, this._bindable) |
1009 : _target = reflect(node), | 1011 : _target = reflect(node) { |
1010 _property = property, | |
1011 super(node, MirrorSystem.getName(property), model, path) { | |
1012 | 1012 |
1013 _sub = node.changes.listen(_propertyValueChanged); | 1013 _sub = node.changes.listen(_propertyValueChanged); |
| 1014 _updateNode(open(_updateNode)); |
1014 } | 1015 } |
1015 | 1016 |
1016 void close() { | 1017 void _updateNode(newValue) { |
1017 if (closed) return; | |
1018 _sub.cancel(); | |
1019 super.close(); | |
1020 } | |
1021 | |
1022 void valueChanged(newValue) { | |
1023 _lastValue = newValue; | 1018 _lastValue = newValue; |
1024 _target.setField(_property, newValue); | 1019 _target.setField(_property, newValue); |
1025 } | 1020 } |
1026 | 1021 |
1027 void _propertyValueChanged(List<ChangeRecord> records) { | 1022 void _propertyValueChanged(List<ChangeRecord> records) { |
1028 for (var record in records) { | 1023 for (var record in records) { |
1029 if (record is PropertyChangeRecord && record.name == _property) { | 1024 if (record is PropertyChangeRecord && record.name == _property) { |
1030 final newValue = _target.getField(_property).reflectee; | 1025 final newValue = _target.getField(_property).reflectee; |
1031 if (!identical(_lastValue, newValue)) { | 1026 if (!identical(_lastValue, newValue)) { |
1032 value = newValue; | 1027 this.value = newValue; |
1033 } | 1028 } |
1034 return; | 1029 return; |
1035 } | 1030 } |
1036 } | 1031 } |
1037 } | 1032 } |
| 1033 |
| 1034 open(callback(value)) => _bindable.open(callback); |
| 1035 get value => _bindable.value; |
| 1036 set value(newValue) => _bindable.value = newValue; |
| 1037 |
| 1038 void close() { |
| 1039 if (_sub != null) { |
| 1040 _sub.cancel(); |
| 1041 _sub = null; |
| 1042 } |
| 1043 _bindable.close(); |
| 1044 } |
1038 } | 1045 } |
1039 | 1046 |
1040 bool _toBoolean(value) => null != value && false != value; | 1047 bool _toBoolean(value) => null != value && false != value; |
1041 | 1048 |
1042 TypeMirror _propertyType(DeclarationMirror property) => | 1049 TypeMirror _propertyType(DeclarationMirror property) => |
1043 property is VariableMirror | 1050 property is VariableMirror ? property.type |
1044 ? (property as VariableMirror).type | |
1045 : (property as MethodMirror).returnType; | 1051 : (property as MethodMirror).returnType; |
1046 | 1052 |
1047 TypeMirror _inferPropertyType(Object value, DeclarationMirror property) { | 1053 TypeMirror _inferPropertyType(Object value, DeclarationMirror property) { |
1048 var type = _propertyType(property); | 1054 var type = _propertyType(property); |
1049 if (type.qualifiedName == #dart.core.Object || | 1055 if (type.qualifiedName == #dart.core.Object || |
1050 type.qualifiedName == #dynamic) { | 1056 type.qualifiedName == #dynamic) { |
1051 // Attempt to infer field type from the default value. | 1057 // Attempt to infer field type from the default value. |
1052 if (value != null) { | 1058 if (value != null) { |
1053 Type t = _getCoreType(value); | 1059 Type t = _getCoreType(value); |
1054 if (t != null) return reflectClass(t); | 1060 if (t != null) return reflectClass(t); |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1088 polymerCreated(); | 1094 polymerCreated(); |
1089 } | 1095 } |
1090 } | 1096 } |
1091 | 1097 |
1092 class _PropertyValue { | 1098 class _PropertyValue { |
1093 Object oldValue, newValue; | 1099 Object oldValue, newValue; |
1094 _PropertyValue(this.oldValue); | 1100 _PropertyValue(this.oldValue); |
1095 } | 1101 } |
1096 | 1102 |
1097 class _PolymerExpressionsWithEventDelegate extends PolymerExpressions { | 1103 class _PolymerExpressionsWithEventDelegate extends PolymerExpressions { |
1098 prepareBinding(String path, name, node) => | 1104 prepareBinding(String path, name, node) { |
1099 Polymer.prepareBinding(path, name, node, super.prepareBinding); | 1105 if (_hasEventPrefix(name)) return Polymer.prepareBinding(path, name, node); |
| 1106 return super.prepareBinding(path, name, node); |
| 1107 } |
1100 } | 1108 } |
| 1109 |
| 1110 class _EventBindable extends Bindable { |
| 1111 final Node _node; |
| 1112 final String _eventName; |
| 1113 final _model; |
| 1114 final String _path; |
| 1115 StreamSubscription _sub; |
| 1116 |
| 1117 _EventBindable(this._node, this._eventName, this._model, this._path); |
| 1118 |
| 1119 _listener(event) { |
| 1120 var ctrlr = _findController(_node); |
| 1121 if (ctrlr is! Polymer) return; |
| 1122 var obj = ctrlr; |
| 1123 var method = _path; |
| 1124 if (_path.startsWith('@')) { |
| 1125 obj = _model; |
| 1126 method = new PropertyPath(_path.substring(1)).getValueFrom(_model); |
| 1127 } |
| 1128 var detail = event is CustomEvent ? |
| 1129 (event as CustomEvent).detail : null; |
| 1130 ctrlr.dispatchMethod(obj, method, [event, detail, _node]); |
| 1131 } |
| 1132 |
| 1133 // TODO(jmesserly): this won't find the correct host unless the ShadowRoot |
| 1134 // was created on a PolymerElement. |
| 1135 static Polymer _findController(Node node) { |
| 1136 while (node.parentNode != null) { |
| 1137 node = node.parentNode; |
| 1138 } |
| 1139 return _shadowHost[node]; |
| 1140 } |
| 1141 |
| 1142 get value => null; |
| 1143 |
| 1144 open(callback) { |
| 1145 _sub = _node.on[_eventName].listen(_listener); |
| 1146 } |
| 1147 |
| 1148 close() { |
| 1149 if (_sub != null) { |
| 1150 if (_eventsLog.isLoggable(Level.FINE)) { |
| 1151 _eventsLog.fine( |
| 1152 'event.remove: [$_node].$_eventName => [$_model].$_path())'); |
| 1153 } |
| 1154 _sub.cancel(); |
| 1155 _sub = null; |
| 1156 } |
| 1157 } |
| 1158 } |
OLD | NEW |