| OLD | NEW |
| (Empty) |
| 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 | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 part of polymer; | |
| 6 | |
| 7 /// Use this annotation to publish a property as an attribute. | |
| 8 /// | |
| 9 /// You can also use [PublishedProperty] to provide additional information, | |
| 10 /// such as automatically syncing the property back to the attribute. | |
| 11 /// | |
| 12 /// For example: | |
| 13 /// | |
| 14 /// class MyPlaybackElement extends PolymerElement { | |
| 15 /// // This will be available as an HTML attribute, for example: | |
| 16 /// // | |
| 17 /// // <my-playback volume="11"> | |
| 18 /// // | |
| 19 /// // It will support initialization and data-binding via <template>: | |
| 20 /// // | |
| 21 /// // <template> | |
| 22 /// // <my-playback volume="{{x}}"> | |
| 23 /// // </template> | |
| 24 /// // | |
| 25 /// // If the template is instantiated or given a model, `x` will be | |
| 26 /// // used for this field and updated whenever `volume` changes. | |
| 27 /// @published | |
| 28 /// double get volume => readValue(#volume); | |
| 29 /// set volume(double newValue) => writeValue(#volume, newValue); | |
| 30 /// | |
| 31 /// // This will be available as an HTML attribute, like above, but it | |
| 32 /// // will also serialize values set to the property to the attribute. | |
| 33 /// // In other words, attributes['volume2'] will contain a serialized | |
| 34 /// // version of this field. | |
| 35 /// @PublishedProperty(reflect: true) | |
| 36 /// double get volume2 => readValue(#volume2); | |
| 37 /// set volume2(double newValue) => writeValue(#volume2, newValue); | |
| 38 /// } | |
| 39 /// | |
| 40 /// **Important note**: the pattern using `readValue` and `writeValue` | |
| 41 /// guarantees that reading the property will give you the latest value at any | |
| 42 /// given time, even if change notifications have not been propagated. | |
| 43 /// | |
| 44 /// We still support using @published on a field, but this will not | |
| 45 /// provide the same guarantees, so this is discouraged. For example: | |
| 46 /// | |
| 47 /// // Avoid this if possible. This will be available as an HTML | |
| 48 /// // attribute too, but you might need to delay reading volume3 | |
| 49 /// // asynchronously to guarantee that you read the latest value | |
| 50 /// // set through bindings. | |
| 51 /// @published double volume3; | |
| 52 const published = const PublishedProperty(); | |
| 53 | |
| 54 /// An annotation used to publish a field as an attribute. See [published]. | |
| 55 class PublishedProperty extends ObservableProperty { | |
| 56 /// Whether the property value should be reflected back to the HTML attribute. | |
| 57 final bool reflect; | |
| 58 | |
| 59 const PublishedProperty({this.reflect: false}); | |
| 60 } | |
| 61 | |
| 62 /// Use this type to observe a property and have the method be called when it | |
| 63 /// changes. For example: | |
| 64 /// | |
| 65 /// @ObserveProperty('foo.bar baz qux') | |
| 66 /// validate() { | |
| 67 /// // use this.foo.bar, this.baz, and this.qux in validation | |
| 68 /// ... | |
| 69 /// } | |
| 70 /// | |
| 71 /// Note that you can observe a property path, and more than a single property | |
| 72 /// can be specified in a space-delimited list or as a constant List. | |
| 73 class ObserveProperty { | |
| 74 final _names; | |
| 75 | |
| 76 List<String> get names { | |
| 77 var n = _names; | |
| 78 // TODO(jmesserly): the bogus '$n' is to workaround a dart2js bug, otherwise | |
| 79 // it generates an incorrect call site. | |
| 80 if (n is String) return '$n'.split(' '); | |
| 81 if (n is! Iterable) { | |
| 82 throw new UnsupportedError('ObserveProperty takes either an Iterable of ' | |
| 83 'names, or a space separated String, instead of `$n`.'); | |
| 84 } | |
| 85 return n; | |
| 86 } | |
| 87 | |
| 88 const ObserveProperty(this._names); | |
| 89 } | |
| 90 | |
| 91 /// Use this to create computed properties that are updated automatically. The | |
| 92 /// annotation includes a polymer expression that describes how this property | |
| 93 /// value can be expressed in terms of the values of other properties. For | |
| 94 /// example: | |
| 95 /// | |
| 96 /// class MyPlaybackElement extends PolymerElement { | |
| 97 /// @observable int x; | |
| 98 /// | |
| 99 /// // Reading xTimes2 will return x * 2. | |
| 100 /// @ComputedProperty('x * 2') | |
| 101 /// int get xTimes2 => readValue(#xTimes2); | |
| 102 /// | |
| 103 /// If the polymer expression is assignable, you can also define a setter for | |
| 104 /// it. For example: | |
| 105 /// | |
| 106 /// // Reading c will return a.b, writing c will update a.b. | |
| 107 /// @ComputedProperty('a.b') | |
| 108 /// get c => readValue(#c); | |
| 109 /// set c(newValue) => writeValue(#c, newValue); | |
| 110 /// | |
| 111 /// The expression can do anything that is allowed in a polymer expresssion, | |
| 112 /// even making calls to methods in your element. However, dependencies that are | |
| 113 /// only used within those methods and that are not visible in the polymer | |
| 114 /// expression, will not be observed. For example: | |
| 115 /// | |
| 116 /// // Because `x` only appears inside method `m`, we will not notice | |
| 117 /// // that `d` has changed if `x` is modified. However, `d` will be | |
| 118 /// // updated whenever `c` changes. | |
| 119 /// @ComputedProperty('m(c)') | |
| 120 /// get d => readValue(#d); | |
| 121 /// | |
| 122 /// m(c) => c + x; | |
| 123 class ComputedProperty { | |
| 124 /// A polymer expression, evaluated in the context of the custom element where | |
| 125 /// this annotation is used. | |
| 126 final String expression; | |
| 127 | |
| 128 const ComputedProperty(this.expression); | |
| 129 } | |
| 130 | |
| 131 /// Base class for PolymerElements deriving from HtmlElement. | |
| 132 /// | |
| 133 /// See [Polymer]. | |
| 134 class PolymerElement extends HtmlElement with Polymer, Observable { | |
| 135 PolymerElement.created() : super.created() { | |
| 136 polymerCreated(); | |
| 137 } | |
| 138 } | |
| 139 | |
| 140 | |
| 141 /// The mixin class for Polymer elements. It provides convenience features on | |
| 142 /// top of the custom elements web standard. | |
| 143 /// | |
| 144 /// If this class is used as a mixin, | |
| 145 /// you must call `polymerCreated()` from the body of your constructor. | |
| 146 abstract class Polymer implements Element, Observable, NodeBindExtension { | |
| 147 | |
| 148 // TODO(jmesserly): should this really be public? | |
| 149 /// Regular expression that matches data-bindings. | |
| 150 static final bindPattern = new RegExp(r'\{\{([^{}]*)}}'); | |
| 151 | |
| 152 /// Like [document.register] but for Polymer elements. | |
| 153 /// | |
| 154 /// Use the [name] to specify custom elment's tag name, for example: | |
| 155 /// "fancy-button" if the tag is used as `<fancy-button>`. | |
| 156 /// | |
| 157 /// The [type] is the type to construct. If not supplied, it defaults to | |
| 158 /// [PolymerElement]. | |
| 159 // NOTE: this is called "element" in src/declaration/polymer-element.js, and | |
| 160 // exported as "Polymer". | |
| 161 static void register(String name, [Type type]) { | |
| 162 //console.log('registering [' + name + ']'); | |
| 163 if (type == null) type = PolymerElement; | |
| 164 | |
| 165 _typesByName[name] = type; | |
| 166 | |
| 167 // Dart note: here we notify JS of the element registration. We don't pass | |
| 168 // the Dart type because we will handle that in PolymerDeclaration. | |
| 169 // See _hookJsPolymerDeclaration for how this is done. | |
| 170 (js.context['Polymer'] as JsFunction).apply([name]); | |
| 171 (js.context['HTMLElement']['register'] as JsFunction).apply( | |
| 172 [name, js.context['HTMLElement']['prototype']]); | |
| 173 } | |
| 174 | |
| 175 /// Register a custom element that has no associated `<polymer-element>`. | |
| 176 /// Unlike [register] this will always perform synchronous registration and | |
| 177 /// by the time this method returns the element will be available using | |
| 178 /// [document.createElement] or by modifying the HTML to include the element. | |
| 179 static void registerSync(String name, Type type, | |
| 180 {String extendsTag, Document doc, Node template}) { | |
| 181 | |
| 182 // Our normal registration, this will queue up the name->type association. | |
| 183 register(name, type); | |
| 184 | |
| 185 // Build a polymer-element and initialize it to register | |
| 186 if (doc == null) doc = document; | |
| 187 var poly = doc.createElement('polymer-element'); | |
| 188 poly.attributes['name'] = name; | |
| 189 if (extendsTag != null) poly.attributes['extends'] = extendsTag; | |
| 190 if (template != null) poly.append(template); | |
| 191 | |
| 192 // TODO(jmesserly): conceptually this is just: | |
| 193 // new JsObject.fromBrowserObject(poly).callMethod('init') | |
| 194 // | |
| 195 // However doing it that way hits an issue with JS-interop in IE10: we get a | |
| 196 // JsObject that wraps something other than `poly`, due to improper caching. | |
| 197 // By reusing _polymerElementProto that we used for 'register', we can | |
| 198 // then call apply on it to invoke init() with the correct `this` pointer. | |
| 199 JsFunction init = _polymerElementProto['init']; | |
| 200 init.apply([], thisArg: poly); | |
| 201 } | |
| 202 | |
| 203 // Warning for when people try to use `importElements` or `import`. | |
| 204 static const String _DYNAMIC_IMPORT_WARNING = 'Dynamically loading html ' | |
| 205 'imports has very limited support right now in dart, see ' | |
| 206 'http://dartbug.com/17873.'; | |
| 207 | |
| 208 /// Loads the set of HTMLImports contained in `node`. Returns a future that | |
| 209 /// resolves when all the imports have been loaded. This method can be used to | |
| 210 /// lazily load imports. For example, given a template: | |
| 211 /// | |
| 212 /// <template> | |
| 213 /// <link rel="import" href="my-import1.html"> | |
| 214 /// <link rel="import" href="my-import2.html"> | |
| 215 /// </template> | |
| 216 /// | |
| 217 /// Polymer.importElements(template.content) | |
| 218 /// .then((_) => print('imports lazily loaded')); | |
| 219 /// | |
| 220 /// Dart Note: This has very limited support in dart, http://dartbug.com/17873 | |
| 221 // Dart Note: From src/lib/import.js For now proxy to the JS methods, | |
| 222 // because we want to share the loader with polymer.js for interop purposes. | |
| 223 static Future importElements(Node elementOrFragment) { | |
| 224 print(_DYNAMIC_IMPORT_WARNING); | |
| 225 var completer = new Completer(); | |
| 226 js.context['Polymer'].callMethod('importElements', | |
| 227 [elementOrFragment, () => completer.complete()]); | |
| 228 return completer.future; | |
| 229 } | |
| 230 | |
| 231 /// Loads an HTMLImport for each url specified in the `urls` array. Notifies | |
| 232 /// when all the imports have loaded by calling the `callback` function | |
| 233 /// argument. This method can be used to lazily load imports. For example, | |
| 234 /// For example, | |
| 235 /// | |
| 236 /// Polymer.import(['my-import1.html', 'my-import2.html']) | |
| 237 /// .then((_) => print('imports lazily loaded')); | |
| 238 /// | |
| 239 /// Dart Note: This has very limited support in dart, http://dartbug.com/17873 | |
| 240 // Dart Note: From src/lib/import.js. For now proxy to the JS methods, | |
| 241 // because we want to share the loader with polymer.js for interop purposes. | |
| 242 static Future import(List urls) { | |
| 243 print(_DYNAMIC_IMPORT_WARNING); | |
| 244 var completer = new Completer(); | |
| 245 js.context['Polymer'].callMethod('import', | |
| 246 [urls, () => completer.complete()]); | |
| 247 return completer.future; | |
| 248 } | |
| 249 | |
| 250 /// Deprecated: Use `import` instead. | |
| 251 @deprecated | |
| 252 static Future importUrls(List urls) { | |
| 253 return import(urls); | |
| 254 } | |
| 255 | |
| 256 static final Completer _onReady = new Completer(); | |
| 257 | |
| 258 /// Future indicating that the Polymer library has been loaded and is ready | |
| 259 /// for use. | |
| 260 static Future get onReady => _onReady.future; | |
| 261 | |
| 262 /// Returns a list of elements that have had polymer-elements created but | |
| 263 /// are not yet ready to register. The list is an array of element | |
| 264 /// definitions. | |
| 265 static List<Element> get waitingFor => | |
| 266 _Polymer.callMethod('waitingFor', [null]); | |
| 267 | |
| 268 /// Forces polymer to register any pending elements. Can be used to abort | |
| 269 /// waiting for elements that are partially defined. | |
| 270 static forceReady([int timeout]) => | |
| 271 _Polymer.callMethod('forceReady', [null, timeout]); | |
| 272 | |
| 273 /// The most derived `<polymer-element>` declaration for this element. | |
| 274 PolymerDeclaration get element => _element; | |
| 275 PolymerDeclaration _element; | |
| 276 | |
| 277 /// Deprecated: use [element] instead. | |
| 278 @deprecated PolymerDeclaration get declaration => _element; | |
| 279 | |
| 280 Map<String, StreamSubscription> _namedObservers; | |
| 281 List<Bindable> _observers = []; | |
| 282 | |
| 283 bool _unbound; // lazy-initialized | |
| 284 PolymerJob _unbindAllJob; | |
| 285 | |
| 286 CompoundObserver _propertyObserver; | |
| 287 bool _readied = false; | |
| 288 | |
| 289 JsObject _jsElem; | |
| 290 | |
| 291 /// Returns the object that should be used as the event controller for | |
| 292 /// event bindings in this element's template. If set, this will override the | |
| 293 /// normal controller lookup. | |
| 294 // TODO(jmesserly): we need to use a JS-writable property as our backing | |
| 295 // store, because of elements such as: | |
| 296 // https://github.com/Polymer/core-overlay/blob/eeb14853/core-overlay-layer.ht
ml#L78 | |
| 297 get eventController => _jsElem['eventController']; | |
| 298 set eventController(value) { _jsElem['eventController'] = value; } | |
| 299 | |
| 300 bool get hasBeenAttached => _hasBeenAttached; | |
| 301 bool _hasBeenAttached = false; | |
| 302 | |
| 303 /// Gets the shadow root associated with the corresponding custom element. | |
| 304 /// | |
| 305 /// This is identical to [shadowRoot], unless there are multiple levels of | |
| 306 /// inheritance and they each have their own shadow root. For example, | |
| 307 /// this can happen if the base class and subclass both have `<template>` tags | |
| 308 /// in their `<polymer-element>` tags. | |
| 309 // TODO(jmesserly): should expose this as an immutable map. | |
| 310 // Similar issue as $. | |
| 311 final Map<String, ShadowRoot> shadowRoots = | |
| 312 new LinkedHashMap<String, ShadowRoot>(); | |
| 313 | |
| 314 /// Map of items in the shadow root(s) by their [Element.id]. | |
| 315 // TODO(jmesserly): various issues: | |
| 316 // * wrap in UnmodifiableMapView? | |
| 317 // * should we have an object that implements noSuchMethod? | |
| 318 // * should the map have a key order (e.g. LinkedHash or SplayTree)? | |
| 319 // * should this be a live list? Polymer doesn't, maybe due to JS limitations? | |
| 320 // Note: this is observable to support $['someId'] being used in templates. | |
| 321 // The template is stamped before $ is populated, so we need observation if | |
| 322 // we want it to be usable in bindings. | |
| 323 final Map<String, dynamic> $ = new ObservableMap<String, dynamic>(); | |
| 324 | |
| 325 /// Use to override the default syntax for polymer-elements. | |
| 326 /// By default this will be null, which causes [instanceTemplate] to use | |
| 327 /// the template's bindingDelegate or the [element.syntax], in that order. | |
| 328 PolymerExpressions get syntax => null; | |
| 329 | |
| 330 bool get _elementPrepared => _element != null; | |
| 331 | |
| 332 /// Retrieves the custom element name. It should be used instead | |
| 333 /// of localName, see: https://github.com/Polymer/polymer-dev/issues/26 | |
| 334 String get _name { | |
| 335 if (_element != null) return _element.name; | |
| 336 var isAttr = attributes['is']; | |
| 337 return (isAttr == null || isAttr == '') ? localName : isAttr; | |
| 338 } | |
| 339 | |
| 340 /// By default the data bindings will be cleaned up when this custom element | |
| 341 /// is detached from the document. Overriding this to return `true` will | |
| 342 /// prevent that from happening. | |
| 343 bool get preventDispose => false; | |
| 344 | |
| 345 /// Properties exposed by this element. | |
| 346 // Dart note: unlike Javascript we can't override the original property on | |
| 347 // the object, so we use this mechanism instead to define properties. See more | |
| 348 // details in [_PropertyAccessor]. | |
| 349 Map<Symbol, _PropertyAccessor> _properties = {}; | |
| 350 | |
| 351 /// Helper to implement a property with the given [name]. This is used for | |
| 352 /// normal and computed properties. Normal properties can provide the initial | |
| 353 /// value using the [initialValue] function. Computed properties ignore | |
| 354 /// [initialValue], their value is derived from the expression in the | |
| 355 /// [ComputedProperty] annotation that appears above the getter that uses this | |
| 356 /// helper. | |
| 357 readValue(Symbol name, [initialValue()]) { | |
| 358 var property = _properties[name]; | |
| 359 if (property == null) { | |
| 360 var value; | |
| 361 // Dart note: most computed properties are created in advance in | |
| 362 // createComputedProperties, but if one computed property depends on | |
| 363 // another, the declaration order might matter. Rather than trying to | |
| 364 // register them in order, we include here also the option of lazily | |
| 365 // creating the property accessor on the first read. | |
| 366 var binding = _getBindingForComputedProperty(name); | |
| 367 if (binding == null) { // normal property | |
| 368 value = initialValue != null ? initialValue() : null; | |
| 369 } else { | |
| 370 value = binding.value; | |
| 371 } | |
| 372 property = _properties[name] = new _PropertyAccessor(name, this, value); | |
| 373 } | |
| 374 return property.value; | |
| 375 } | |
| 376 | |
| 377 /// Helper to implement a setter of a property with the given [name] on a | |
| 378 /// polymer element. This can be used on normal properties and also on | |
| 379 /// computed properties, as long as the expression used for the computed | |
| 380 /// property is assignable (see [ComputedProperty]). | |
| 381 writeValue(Symbol name, newValue) { | |
| 382 var property = _properties[name]; | |
| 383 if (property == null) { | |
| 384 // Note: computed properties are created in advance in | |
| 385 // createComputedProperties, so we should only need to create here | |
| 386 // non-computed properties. | |
| 387 property = _properties[name] = new _PropertyAccessor(name, this, null); | |
| 388 } | |
| 389 property.value = newValue; | |
| 390 } | |
| 391 | |
| 392 /// If this class is used as a mixin, this method must be called from inside | |
| 393 /// of the `created()` constructor. | |
| 394 /// | |
| 395 /// If this class is a superclass, calling `super.created()` is sufficient. | |
| 396 void polymerCreated() { | |
| 397 var t = nodeBind(this).templateInstance; | |
| 398 if (t != null && t.model != null) { | |
| 399 window.console.warn('Attributes on $_name were data bound ' | |
| 400 'prior to Polymer upgrading the element. This may result in ' | |
| 401 'incorrect binding types.'); | |
| 402 } | |
| 403 prepareElement(); | |
| 404 if (!isTemplateStagingDocument(ownerDocument)) { | |
| 405 _makeElementReady(); | |
| 406 } | |
| 407 } | |
| 408 | |
| 409 /// *Deprecated* use [shadowRoots] instead. | |
| 410 @deprecated | |
| 411 ShadowRoot getShadowRoot(String customTagName) => shadowRoots[customTagName]; | |
| 412 | |
| 413 void prepareElement() { | |
| 414 if (_elementPrepared) { | |
| 415 window.console.warn('Element already prepared: $_name'); | |
| 416 return; | |
| 417 } | |
| 418 _initJsObject(); | |
| 419 // Dart note: get the corresponding <polymer-element> declaration. | |
| 420 _element = _getDeclaration(_name); | |
| 421 // install property storage | |
| 422 createPropertyObserver(); | |
| 423 openPropertyObserver(); | |
| 424 // install boilerplate attributes | |
| 425 copyInstanceAttributes(); | |
| 426 // process input attributes | |
| 427 takeAttributes(); | |
| 428 // add event listeners | |
| 429 addHostListeners(); | |
| 430 } | |
| 431 | |
| 432 /// Initialize JS interop for this element. For now we just initialize the | |
| 433 /// JsObject, but in the future we could also initialize JS APIs here. | |
| 434 _initJsObject() { | |
| 435 _jsElem = new JsObject.fromBrowserObject(this); | |
| 436 } | |
| 437 | |
| 438 /// Deprecated: This is no longer a public method. | |
| 439 @deprecated | |
| 440 makeElementReady() => _makeElementReady(); | |
| 441 | |
| 442 _makeElementReady() { | |
| 443 if (_readied) return; | |
| 444 _readied = true; | |
| 445 createComputedProperties(); | |
| 446 | |
| 447 parseDeclarations(_element); | |
| 448 // NOTE: Support use of the `unresolved` attribute to help polyfill | |
| 449 // custom elements' `:unresolved` feature. | |
| 450 attributes.remove('unresolved'); | |
| 451 // user entry point | |
| 452 _readyLog.info(() => '[$this]: ready'); | |
| 453 ready(); | |
| 454 } | |
| 455 | |
| 456 /// Lifecycle method called when the element has populated it's `shadowRoot`, | |
| 457 /// prepared data-observation, and made itself ready for API interaction. | |
| 458 /// To wait until the element has been attached to the default view, use | |
| 459 /// [attached] or [domReady]. | |
| 460 void ready() {} | |
| 461 | |
| 462 /// Implement to access custom elements in dom descendants, ancestors, | |
| 463 /// or siblings. Because custom elements upgrade in document order, | |
| 464 /// elements accessed in `ready` or `attached` may not be upgraded. When | |
| 465 /// `domReady` is called, all registered custom elements are guaranteed | |
| 466 /// to have been upgraded. | |
| 467 void domReady() {} | |
| 468 | |
| 469 void attached() { | |
| 470 if (!_elementPrepared) { | |
| 471 // Dart specific message for a common issue. | |
| 472 throw new StateError('polymerCreated was not called for custom element ' | |
| 473 '$_name, this should normally be done in the .created() if Polymer ' | |
| 474 'is used as a mixin.'); | |
| 475 } | |
| 476 | |
| 477 cancelUnbindAll(); | |
| 478 if (!hasBeenAttached) { | |
| 479 _hasBeenAttached = true; | |
| 480 async((_) => domReady()); | |
| 481 } | |
| 482 } | |
| 483 | |
| 484 void detached() { | |
| 485 if (!preventDispose) asyncUnbindAll(); | |
| 486 } | |
| 487 | |
| 488 /// Walks the prototype-chain of this element and allows specific | |
| 489 /// classes a chance to process static declarations. | |
| 490 /// | |
| 491 /// In particular, each polymer-element has it's own `template`. | |
| 492 /// `parseDeclarations` is used to accumulate all element `template`s | |
| 493 /// from an inheritance chain. | |
| 494 /// | |
| 495 /// `parseDeclaration` static methods implemented in the chain are called | |
| 496 /// recursively, oldest first, with the `<polymer-element>` associated | |
| 497 /// with the current prototype passed as an argument. | |
| 498 /// | |
| 499 /// An element may override this method to customize shadow-root generation. | |
| 500 void parseDeclarations(PolymerDeclaration declaration) { | |
| 501 if (declaration != null) { | |
| 502 parseDeclarations(declaration.superDeclaration); | |
| 503 parseDeclaration(declaration.element); | |
| 504 } | |
| 505 } | |
| 506 | |
| 507 /// Perform init-time actions based on static information in the | |
| 508 /// `<polymer-element>` instance argument. | |
| 509 /// | |
| 510 /// For example, the standard implementation locates the template associated | |
| 511 /// with the given `<polymer-element>` and stamps it into a shadow-root to | |
| 512 /// implement shadow inheritance. | |
| 513 /// | |
| 514 /// An element may override this method for custom behavior. | |
| 515 void parseDeclaration(Element elementElement) { | |
| 516 var template = fetchTemplate(elementElement); | |
| 517 | |
| 518 if (template != null) { | |
| 519 var root = shadowFromTemplate(template); | |
| 520 | |
| 521 var name = elementElement.attributes['name']; | |
| 522 if (name == null) return; | |
| 523 shadowRoots[name] = root; | |
| 524 } | |
| 525 } | |
| 526 | |
| 527 /// Given a `<polymer-element>`, find an associated template (if any) to be | |
| 528 /// used for shadow-root generation. | |
| 529 /// | |
| 530 /// An element may override this method for custom behavior. | |
| 531 Element fetchTemplate(Element elementElement) => | |
| 532 elementElement.querySelector('template'); | |
| 533 | |
| 534 /// Utility function that stamps a `<template>` into light-dom. | |
| 535 Node lightFromTemplate(Element template, [Node refNode]) { | |
| 536 if (template == null) return null; | |
| 537 | |
| 538 // TODO(sorvell): mark this element as an event controller so that | |
| 539 // event listeners on bound nodes inside it will be called on it. | |
| 540 // Note, the expectation here is that events on all descendants | |
| 541 // should be handled by this element. | |
| 542 eventController = this; | |
| 543 | |
| 544 // stamp template | |
| 545 // which includes parsing and applying MDV bindings before being | |
| 546 // inserted (to avoid {{}} in attribute values) | |
| 547 // e.g. to prevent <img src="images/{{icon}}"> from generating a 404. | |
| 548 var dom = instanceTemplate(template); | |
| 549 // append to shadow dom | |
| 550 if (refNode != null) { | |
| 551 append(dom); | |
| 552 } else { | |
| 553 insertBefore(dom, refNode); | |
| 554 } | |
| 555 // perform post-construction initialization tasks on ahem, light root | |
| 556 shadowRootReady(this); | |
| 557 // return the created shadow root | |
| 558 return dom; | |
| 559 } | |
| 560 | |
| 561 /// Utility function that creates a shadow root from a `<template>`. | |
| 562 /// | |
| 563 /// The base implementation will return a [ShadowRoot], but you can replace it | |
| 564 /// with your own code and skip ShadowRoot creation. In that case, you should | |
| 565 /// return `null`. | |
| 566 /// | |
| 567 /// In your overridden method, you can use [instanceTemplate] to stamp the | |
| 568 /// template and initialize data binding, and [shadowRootReady] to intialize | |
| 569 /// other Polymer features like event handlers. It is fine to call | |
| 570 /// shadowRootReady with a node other than a ShadowRoot such as with `this`. | |
| 571 ShadowRoot shadowFromTemplate(Element template) { | |
| 572 if (template == null) return null; | |
| 573 // make a shadow root | |
| 574 var root = createShadowRoot(); | |
| 575 // stamp template | |
| 576 // which includes parsing and applying MDV bindings before being | |
| 577 // inserted (to avoid {{}} in attribute values) | |
| 578 // e.g. to prevent <img src="images/{{icon}}"> from generating a 404. | |
| 579 var dom = instanceTemplate(template); | |
| 580 // append to shadow dom | |
| 581 root.append(dom); | |
| 582 // perform post-construction initialization tasks on shadow root | |
| 583 shadowRootReady(root); | |
| 584 // return the created shadow root | |
| 585 return root; | |
| 586 } | |
| 587 | |
| 588 void shadowRootReady(Node root) { | |
| 589 // locate nodes with id and store references to them in this.$ hash | |
| 590 marshalNodeReferences(root); | |
| 591 } | |
| 592 | |
| 593 /// Locate nodes with id and store references to them in [$] hash. | |
| 594 void marshalNodeReferences(Node root) { | |
| 595 if (root == null) return; | |
| 596 for (var n in (root as dynamic).querySelectorAll('[id]')) { | |
| 597 $[n.id] = n; | |
| 598 } | |
| 599 } | |
| 600 | |
| 601 void attributeChanged(String name, String oldValue, String newValue) { | |
| 602 if (name != 'class' && name != 'style') { | |
| 603 attributeToProperty(name, newValue); | |
| 604 } | |
| 605 } | |
| 606 | |
| 607 // TODO(jmesserly): this could be a top level method. | |
| 608 /// Returns a future when `node` changes, or when its children or subtree | |
| 609 /// changes. | |
| 610 /// | |
| 611 /// Use [MutationObserver] if you want to listen to a stream of changes. | |
| 612 Future<List<MutationRecord>> onMutation(Node node) { | |
| 613 var completer = new Completer(); | |
| 614 new MutationObserver((mutations, observer) { | |
| 615 observer.disconnect(); | |
| 616 completer.complete(mutations); | |
| 617 })..observe(node, childList: true, subtree: true); | |
| 618 return completer.future; | |
| 619 } | |
| 620 | |
| 621 // copy attributes defined in the element declaration to the instance | |
| 622 // e.g. <polymer-element name="x-foo" tabIndex="0"> tabIndex is copied | |
| 623 // to the element instance here. | |
| 624 void copyInstanceAttributes() { | |
| 625 _element._instanceAttributes.forEach((name, value) { | |
| 626 attributes.putIfAbsent(name, () => value); | |
| 627 }); | |
| 628 } | |
| 629 | |
| 630 void takeAttributes() { | |
| 631 if (_element._publishLC == null) return; | |
| 632 attributes.forEach(attributeToProperty); | |
| 633 } | |
| 634 | |
| 635 /// If attribute [name] is mapped to a property, deserialize | |
| 636 /// [value] into that property. | |
| 637 void attributeToProperty(String name, String value) { | |
| 638 // try to match this attribute to a property (attributes are | |
| 639 // all lower-case, so this is case-insensitive search) | |
| 640 var decl = propertyForAttribute(name); | |
| 641 if (decl == null) return; | |
| 642 | |
| 643 // filter out 'mustached' values, these are to be | |
| 644 // replaced with bound-data and are not yet values | |
| 645 // themselves. | |
| 646 if (value == null || value.contains(Polymer.bindPattern)) return; | |
| 647 | |
| 648 final currentValue = smoke.read(this, decl.name); | |
| 649 | |
| 650 // deserialize Boolean or Number values from attribute | |
| 651 var type = decl.type; | |
| 652 if ((type == Object || type == dynamic) && currentValue != null) { | |
| 653 // Attempt to infer field type from the current value. | |
| 654 type = currentValue.runtimeType; | |
| 655 } | |
| 656 final newValue = deserializeValue(value, currentValue, type); | |
| 657 | |
| 658 // only act if the value has changed | |
| 659 if (!identical(newValue, currentValue)) { | |
| 660 // install new value (has side-effects) | |
| 661 smoke.write(this, decl.name, newValue); | |
| 662 } | |
| 663 } | |
| 664 | |
| 665 /// Return the published property matching name, or null. | |
| 666 // TODO(jmesserly): should we just return Symbol here? | |
| 667 smoke.Declaration propertyForAttribute(String name) { | |
| 668 final publishLC = _element._publishLC; | |
| 669 if (publishLC == null) return null; | |
| 670 return publishLC[name]; | |
| 671 } | |
| 672 | |
| 673 /// Convert representation of [value] based on [type] and [currentValue]. | |
| 674 Object deserializeValue(String value, Object currentValue, Type type) => | |
| 675 deserialize.deserializeValue(value, currentValue, type); | |
| 676 | |
| 677 String serializeValue(Object value) { | |
| 678 if (value == null) return null; | |
| 679 | |
| 680 if (value is bool) { | |
| 681 return _toBoolean(value) ? '' : null; | |
| 682 } else if (value is String || value is num) { | |
| 683 return '$value'; | |
| 684 } | |
| 685 return null; | |
| 686 } | |
| 687 | |
| 688 void reflectPropertyToAttribute(String path) { | |
| 689 // TODO(sjmiles): consider memoizing this | |
| 690 // try to intelligently serialize property value | |
| 691 final propValue = new PropertyPath(path).getValueFrom(this); | |
| 692 final serializedValue = serializeValue(propValue); | |
| 693 // boolean properties must reflect as boolean attributes | |
| 694 if (serializedValue != null) { | |
| 695 attributes[path] = serializedValue; | |
| 696 // TODO(sorvell): we should remove attr for all properties | |
| 697 // that have undefined serialization; however, we will need to | |
| 698 // refine the attr reflection system to achieve this; pica, for example, | |
| 699 // relies on having inferredType object properties not removed as | |
| 700 // attrs. | |
| 701 } else if (propValue is bool) { | |
| 702 attributes.remove(path); | |
| 703 } | |
| 704 } | |
| 705 | |
| 706 /// Creates the document fragment to use for each instance of the custom | |
| 707 /// element, given the `<template>` node. By default this is equivalent to: | |
| 708 /// | |
| 709 /// templateBind(template).createInstance(this, polymerSyntax); | |
| 710 /// | |
| 711 /// Where polymerSyntax is a singleton [PolymerExpressions] instance. | |
| 712 /// | |
| 713 /// You can override this method to change the instantiation behavior of the | |
| 714 /// template, for example to use a different data-binding syntax. | |
| 715 DocumentFragment instanceTemplate(Element template) { | |
| 716 // ensure template is decorated (lets things like <tr template ...> work) | |
| 717 TemplateBindExtension.decorate(template); | |
| 718 var syntax = this.syntax; | |
| 719 var t = templateBind(template); | |
| 720 if (syntax == null && t.bindingDelegate == null) { | |
| 721 syntax = element.syntax; | |
| 722 } | |
| 723 var dom = t.createInstance(this, syntax); | |
| 724 _observers.addAll(getTemplateInstanceBindings(dom)); | |
| 725 return dom; | |
| 726 } | |
| 727 | |
| 728 /// Called by TemplateBinding/NodeBind to setup a binding to the given | |
| 729 /// property. It's overridden here to support property bindings in addition to | |
| 730 /// attribute bindings that are supported by default. | |
| 731 Bindable bind(String name, bindable, {bool oneTime: false}) { | |
| 732 var decl = propertyForAttribute(name); | |
| 733 if (decl == null) { | |
| 734 // Cannot call super.bind because template_binding is its own package | |
| 735 return nodeBindFallback(this).bind(name, bindable, oneTime: oneTime); | |
| 736 } else { | |
| 737 // use n-way Polymer binding | |
| 738 var observer = bindProperty(decl.name, bindable, oneTime: oneTime); | |
| 739 // NOTE: reflecting binding information is typically required only for | |
| 740 // tooling. It has a performance cost so it's opt-in in Node.bind. | |
| 741 if (enableBindingsReflection && observer != null) { | |
| 742 // Dart note: this is not needed because of how _PolymerBinding works. | |
| 743 //observer.path = bindable.path_; | |
| 744 _recordBinding(name, observer); | |
| 745 } | |
| 746 var reflect = _element._reflect; | |
| 747 | |
| 748 // Get back to the (possibly camel-case) name for the property. | |
| 749 var propName = smoke.symbolToName(decl.name); | |
| 750 if (reflect != null && reflect.contains(propName)) { | |
| 751 reflectPropertyToAttribute(propName); | |
| 752 } | |
| 753 return observer; | |
| 754 } | |
| 755 } | |
| 756 | |
| 757 _recordBinding(String name, observer) { | |
| 758 if (bindings == null) bindings = {}; | |
| 759 this.bindings[name] = observer; | |
| 760 } | |
| 761 | |
| 762 /// Called by TemplateBinding when all bindings on an element have been | |
| 763 /// executed. This signals that all element inputs have been gathered and it's | |
| 764 /// safe to ready the element, create shadow-root and start data-observation. | |
| 765 bindFinished() => _makeElementReady(); | |
| 766 | |
| 767 Map<String, Bindable> get bindings => nodeBindFallback(this).bindings; | |
| 768 set bindings(Map value) { nodeBindFallback(this).bindings = value; } | |
| 769 | |
| 770 TemplateInstance get templateInstance => | |
| 771 nodeBindFallback(this).templateInstance; | |
| 772 | |
| 773 /// Called at detached time to signal that an element's bindings should be | |
| 774 /// cleaned up. This is done asynchronously so that users have the chance to | |
| 775 /// call `cancelUnbindAll` to prevent unbinding. | |
| 776 void asyncUnbindAll() { | |
| 777 if (_unbound == true) return; | |
| 778 _unbindLog.fine(() => '[$_name] asyncUnbindAll'); | |
| 779 _unbindAllJob = scheduleJob(_unbindAllJob, unbindAll); | |
| 780 } | |
| 781 | |
| 782 /// This method should rarely be used and only if `cancelUnbindAll` has been | |
| 783 /// called to prevent element unbinding. In this case, the element's bindings | |
| 784 /// will not be automatically cleaned up and it cannot be garbage collected by | |
| 785 /// by the system. If memory pressure is a concern or a large amount of | |
| 786 /// elements need to be managed in this way, `unbindAll` can be called to | |
| 787 /// deactivate the element's bindings and allow its memory to be reclaimed. | |
| 788 void unbindAll() { | |
| 789 if (_unbound == true) return; | |
| 790 closeObservers(); | |
| 791 closeNamedObservers(); | |
| 792 _unbound = true; | |
| 793 } | |
| 794 | |
| 795 //// Call in `detached` to prevent the element from unbinding when it is | |
| 796 //// detached from the dom. The element is unbound as a cleanup step that | |
| 797 //// allows its memory to be reclaimed. If `cancelUnbindAll` is used, consider | |
| 798 /// calling `unbindAll` when the element is no longer needed. This will allow | |
| 799 /// its memory to be reclaimed. | |
| 800 void cancelUnbindAll() { | |
| 801 if (_unbound == true) { | |
| 802 _unbindLog.warning(() => | |
| 803 '[$_name] already unbound, cannot cancel unbindAll'); | |
| 804 return; | |
| 805 } | |
| 806 _unbindLog.fine(() => '[$_name] cancelUnbindAll'); | |
| 807 if (_unbindAllJob != null) { | |
| 808 _unbindAllJob.stop(); | |
| 809 _unbindAllJob = null; | |
| 810 } | |
| 811 } | |
| 812 | |
| 813 static void _forNodeTree(Node node, void callback(Node node)) { | |
| 814 if (node == null) return; | |
| 815 | |
| 816 callback(node); | |
| 817 for (var child = node.firstChild; child != null; child = child.nextNode) { | |
| 818 _forNodeTree(child, callback); | |
| 819 } | |
| 820 } | |
| 821 | |
| 822 /// Creates a CompoundObserver to observe property changes. | |
| 823 /// NOTE, this is only done if there are any properties in the `_observe` | |
| 824 /// object. | |
| 825 void createPropertyObserver() { | |
| 826 final observe = _element._observe; | |
| 827 if (observe != null) { | |
| 828 var o = _propertyObserver = new CompoundObserver(); | |
| 829 // keep track of property observer so we can shut it down | |
| 830 _observers.add(o); | |
| 831 | |
| 832 for (var path in observe.keys) { | |
| 833 o.addPath(this, path); | |
| 834 | |
| 835 // TODO(jmesserly): on the Polymer side it doesn't look like they | |
| 836 // will observe arrays unless it is a length == 1 path. | |
| 837 observeArrayValue(path, path.getValueFrom(this), null); | |
| 838 } | |
| 839 } | |
| 840 } | |
| 841 | |
| 842 /// Start observing property changes. | |
| 843 void openPropertyObserver() { | |
| 844 if (_propertyObserver != null) { | |
| 845 _propertyObserver.open(notifyPropertyChanges); | |
| 846 } | |
| 847 | |
| 848 // Dart note: we need an extra listener only to continue supporting | |
| 849 // @published properties that follow the old syntax until we get rid of it. | |
| 850 // This workaround has timing issues so we prefer the new, not so nice, | |
| 851 // syntax. | |
| 852 if (_element._publish != null) { | |
| 853 changes.listen(_propertyChangeWorkaround); | |
| 854 } | |
| 855 } | |
| 856 | |
| 857 /// Handler for property changes; routes changes to observing methods. | |
| 858 /// Note: array valued properties are observed for array splices. | |
| 859 void notifyPropertyChanges(List newValues, Map oldValues, List paths) { | |
| 860 final observe = _element._observe; | |
| 861 final called = new HashSet(); | |
| 862 | |
| 863 oldValues.forEach((i, oldValue) { | |
| 864 final newValue = newValues[i]; | |
| 865 | |
| 866 // Date note: we don't need any special checking for null and undefined. | |
| 867 | |
| 868 // note: paths is of form [object, path, object, path] | |
| 869 final path = paths[2 * i + 1]; | |
| 870 if (observe == null) return; | |
| 871 | |
| 872 var methods = observe[path]; | |
| 873 if (methods == null) return; | |
| 874 | |
| 875 for (var method in methods) { | |
| 876 if (!called.add(method)) continue; // don't invoke more than once. | |
| 877 | |
| 878 observeArrayValue(path, newValue, oldValue); | |
| 879 // Dart note: JS passes "arguments", so we pass along our args. | |
| 880 // TODO(sorvell): call method with the set of values it's expecting; | |
| 881 // e.g. 'foo bar': 'invalidate' expects the new and old values for | |
| 882 // foo and bar. Currently we give only one of these and then | |
| 883 // deliver all the arguments. | |
| 884 smoke.invoke(this, method, | |
| 885 [oldValue, newValue, newValues, oldValues, paths], adjust: true); | |
| 886 } | |
| 887 }); | |
| 888 } | |
| 889 | |
| 890 /// Force any pending property changes to synchronously deliver to handlers | |
| 891 /// specified in the `observe` object. | |
| 892 /// Note: normally changes are processed at microtask time. | |
| 893 /// | |
| 894 // Dart note: had to rename this to avoid colliding with | |
| 895 // Observable.deliverChanges. Even worse, super calls aren't possible or | |
| 896 // it prevents Polymer from being a mixin, so we can't override it even if | |
| 897 // we wanted to. | |
| 898 void deliverPropertyChanges() { | |
| 899 if (_propertyObserver != null) { | |
| 900 _propertyObserver.deliver(); | |
| 901 } | |
| 902 } | |
| 903 | |
| 904 // Dart note: this workaround is only for old-style @published properties, | |
| 905 // which have timing issues. See _bindOldStylePublishedProperty below. | |
| 906 // TODO(sigmund): deprecate this. | |
| 907 void _propertyChangeWorkaround(List<ChangeRecord> records) { | |
| 908 for (var record in records) { | |
| 909 if (record is! PropertyChangeRecord) continue; | |
| 910 | |
| 911 var name = record.name; | |
| 912 // The setter of a new-style property will create an accessor in | |
| 913 // _properties[name]. We can skip the workaround for those properties. | |
| 914 if (_properties[name] != null) continue; | |
| 915 _propertyChange(name, record.newValue, record.oldValue); | |
| 916 } | |
| 917 } | |
| 918 | |
| 919 void _propertyChange(Symbol nameSymbol, newValue, oldValue) { | |
| 920 _watchLog.info( | |
| 921 () => '[$this]: $nameSymbol changed from: $oldValue to: $newValue'); | |
| 922 var name = smoke.symbolToName(nameSymbol); | |
| 923 var reflect = _element._reflect; | |
| 924 if (reflect != null && reflect.contains(name)) { | |
| 925 reflectPropertyToAttribute(name); | |
| 926 } | |
| 927 } | |
| 928 | |
| 929 void observeArrayValue(PropertyPath name, Object value, Object old) { | |
| 930 final observe = _element._observe; | |
| 931 if (observe == null) return; | |
| 932 | |
| 933 // we only care if there are registered side-effects | |
| 934 var callbacks = observe[name]; | |
| 935 if (callbacks == null) return; | |
| 936 | |
| 937 // if we are observing the previous value, stop | |
| 938 if (old is ObservableList) { | |
| 939 _observeLog.fine(() => '[$_name] observeArrayValue: unregister $name'); | |
| 940 | |
| 941 closeNamedObserver('${name}__array'); | |
| 942 } | |
| 943 // if the new value is an array, begin observing it | |
| 944 if (value is ObservableList) { | |
| 945 _observeLog.fine(() => '[$_name] observeArrayValue: register $name'); | |
| 946 var sub = value.listChanges.listen((changes) { | |
| 947 for (var callback in callbacks) { | |
| 948 smoke.invoke(this, callback, [changes], adjust: true); | |
| 949 } | |
| 950 }); | |
| 951 registerNamedObserver('${name}__array', sub); | |
| 952 } | |
| 953 } | |
| 954 | |
| 955 emitPropertyChangeRecord(Symbol name, newValue, oldValue) { | |
| 956 if (identical(oldValue, newValue)) return; | |
| 957 _propertyChange(name, newValue, oldValue); | |
| 958 } | |
| 959 | |
| 960 bindToAccessor(Symbol name, Bindable bindable, {resolveBindingValue: false}) { | |
| 961 // Dart note: our pattern is to declare the initial value in the getter. We | |
| 962 // read it via smoke to ensure that the value is initialized correctly. | |
| 963 var oldValue = smoke.read(this, name); | |
| 964 var property = _properties[name]; | |
| 965 if (property == null) { | |
| 966 // We know that _properties[name] is null only for old-style @published | |
| 967 // properties. This fallback is here to make it easier to deprecate the | |
| 968 // old-style of published properties, which have bad timing guarantees | |
| 969 // (see comment in _PolymerBinding). | |
| 970 return _bindOldStylePublishedProperty(name, bindable, oldValue); | |
| 971 } | |
| 972 | |
| 973 property.bindable = bindable; | |
| 974 var value = bindable.open(property.updateValue); | |
| 975 | |
| 976 if (resolveBindingValue) { | |
| 977 // capture A's value if B's value is null or undefined, | |
| 978 // otherwise use B's value | |
| 979 var v = (value == null ? oldValue : value); | |
| 980 if (!identical(value, oldValue)) { | |
| 981 bindable.value = value = v; | |
| 982 } | |
| 983 } | |
| 984 | |
| 985 property.updateValue(value); | |
| 986 var o = new _CloseOnlyBinding(property); | |
| 987 _observers.add(o); | |
| 988 return o; | |
| 989 } | |
| 990 | |
| 991 // Dart note: this fallback uses our old-style binding mechanism to be able to | |
| 992 // link @published properties with bindings. This mechanism is backwards from | |
| 993 // what Javascript does because we can't override the original property. This | |
| 994 // workaround also brings some timing issues which are described in detail in | |
| 995 // dartbug.com/18343. | |
| 996 // TODO(sigmund): deprecate old-style @published properties. | |
| 997 _bindOldStylePublishedProperty(Symbol name, Bindable bindable, oldValue) { | |
| 998 // capture A's value if B's value is null or undefined, | |
| 999 // otherwise use B's value | |
| 1000 if (bindable.value == null) bindable.value = oldValue; | |
| 1001 | |
| 1002 var o = new _PolymerBinding(this, name, bindable); | |
| 1003 _observers.add(o); | |
| 1004 return o; | |
| 1005 } | |
| 1006 | |
| 1007 _getBindingForComputedProperty(Symbol name) { | |
| 1008 var exprString = element._computed[name]; | |
| 1009 if (exprString == null) return null; | |
| 1010 var expr = PolymerExpressions.getExpression(exprString); | |
| 1011 return PolymerExpressions.getBinding(expr, this, | |
| 1012 globals: element.syntax.globals); | |
| 1013 } | |
| 1014 | |
| 1015 createComputedProperties() { | |
| 1016 var computed = this.element._computed; | |
| 1017 for (var name in computed.keys) { | |
| 1018 try { | |
| 1019 // Dart note: this is done in Javascript by modifying the prototype in | |
| 1020 // declaration/properties.js, we can't do that, so we do it here. | |
| 1021 var binding = _getBindingForComputedProperty(name); | |
| 1022 | |
| 1023 // Follow up note: ideally we would only create the accessor object | |
| 1024 // here, but some computed properties might depend on others and | |
| 1025 // evaluating `binding.value` could try to read the value of another | |
| 1026 // computed property that we haven't created yet. For this reason we | |
| 1027 // also allow to also create the accessor in [readValue]. | |
| 1028 if (_properties[name] == null) { | |
| 1029 _properties[name] = new _PropertyAccessor(name, this, binding.value); | |
| 1030 } | |
| 1031 bindToAccessor(name, binding); | |
| 1032 } catch (e) { | |
| 1033 window.console.error('Failed to create computed property $name' | |
| 1034 ' (${computed[name]}): $e'); | |
| 1035 } | |
| 1036 } | |
| 1037 } | |
| 1038 | |
| 1039 // Dart note: to simplify the code above we made registerObserver calls | |
| 1040 // directly invoke _observers.add/addAll. | |
| 1041 void closeObservers() { | |
| 1042 for (var o in _observers) { | |
| 1043 if (o != null) o.close(); | |
| 1044 } | |
| 1045 _observers = []; | |
| 1046 } | |
| 1047 | |
| 1048 /// Bookkeeping observers for memory management. | |
| 1049 void registerNamedObserver(String name, StreamSubscription sub) { | |
| 1050 if (_namedObservers == null) { | |
| 1051 _namedObservers = new Map<String, StreamSubscription>(); | |
| 1052 } | |
| 1053 _namedObservers[name] = sub; | |
| 1054 } | |
| 1055 | |
| 1056 bool closeNamedObserver(String name) { | |
| 1057 var sub = _namedObservers.remove(name); | |
| 1058 if (sub == null) return false; | |
| 1059 sub.cancel(); | |
| 1060 return true; | |
| 1061 } | |
| 1062 | |
| 1063 void closeNamedObservers() { | |
| 1064 if (_namedObservers == null) return; | |
| 1065 for (var sub in _namedObservers.values) { | |
| 1066 if (sub != null) sub.cancel(); | |
| 1067 } | |
| 1068 _namedObservers.clear(); | |
| 1069 _namedObservers = null; | |
| 1070 } | |
| 1071 | |
| 1072 /// Bind the [name] property in this element to [bindable]. *Note* in Dart it | |
| 1073 /// is necessary to also define the field: | |
| 1074 /// | |
| 1075 /// var myProperty; | |
| 1076 /// | |
| 1077 /// ready() { | |
| 1078 /// super.ready(); | |
| 1079 /// bindProperty(#myProperty, | |
| 1080 /// new PathObserver(this, 'myModel.path.to.otherProp')); | |
| 1081 /// } | |
| 1082 Bindable bindProperty(Symbol name, bindableOrValue, {oneTime: false}) { | |
| 1083 // Dart note: normally we only reach this code when we know it's a | |
| 1084 // property, but if someone uses bindProperty directly they might get a | |
| 1085 // NoSuchMethodError either from the getField below, or from the setField | |
| 1086 // inside PolymerBinding. That doesn't seem unreasonable, but it's a slight | |
| 1087 // difference from Polymer.js behavior. | |
| 1088 | |
| 1089 _bindLog.fine(() => 'bindProperty: [$bindableOrValue] to [$_name].[$name]'); | |
| 1090 | |
| 1091 if (oneTime) { | |
| 1092 if (bindableOrValue is Bindable) { | |
| 1093 _bindLog.warning(() => | |
| 1094 'bindProperty: expected non-bindable value n a one-time binding to ' | |
| 1095 '[$_name].[$name], but found $bindableOrValue.'); | |
| 1096 } | |
| 1097 smoke.write(this, name, bindableOrValue); | |
| 1098 return null; | |
| 1099 } | |
| 1100 | |
| 1101 return bindToAccessor(name, bindableOrValue, resolveBindingValue: true); | |
| 1102 } | |
| 1103 | |
| 1104 /// Attach event listeners on the host (this) element. | |
| 1105 void addHostListeners() { | |
| 1106 var events = _element._eventDelegates; | |
| 1107 if (events.isEmpty) return; | |
| 1108 | |
| 1109 _eventsLog.fine(() => '[$_name] addHostListeners: $events'); | |
| 1110 | |
| 1111 // NOTE: host events look like bindings but really are not; | |
| 1112 // (1) we don't want the attribute to be set and (2) we want to support | |
| 1113 // multiple event listeners ('host' and 'instance') and Node.bind | |
| 1114 // by default supports 1 thing being bound. | |
| 1115 events.forEach((type, methodName) { | |
| 1116 // Dart note: the getEventHandler method is on our PolymerExpressions. | |
| 1117 _PolymerGestures.callMethod( | |
| 1118 'addEventListener', | |
| 1119 [this, type, Zone.current.bindUnaryCallback( | |
| 1120 element.syntax.getEventHandler(this, this, methodName))]); | |
| 1121 }); | |
| 1122 } | |
| 1123 | |
| 1124 /// Calls [methodOrCallback] with [args] if it is a closure, otherwise, treat | |
| 1125 /// it as a method name in [object], and invoke it. | |
| 1126 void dispatchMethod(object, callbackOrMethod, List args) { | |
| 1127 _eventsLog.info(() => '>>> [$_name]: dispatch $callbackOrMethod'); | |
| 1128 | |
| 1129 if (callbackOrMethod is Function) { | |
| 1130 int maxArgs = smoke.maxArgs(callbackOrMethod); | |
| 1131 if (maxArgs == -1) { | |
| 1132 _eventsLog.warning( | |
| 1133 'invalid callback: expected callback of 0, 1, 2, or 3 arguments'); | |
| 1134 } | |
| 1135 args.length = maxArgs; | |
| 1136 Function.apply(callbackOrMethod, args); | |
| 1137 } else if (callbackOrMethod is String) { | |
| 1138 smoke.invoke(object, smoke.nameToSymbol(callbackOrMethod), args, | |
| 1139 adjust: true); | |
| 1140 } else { | |
| 1141 _eventsLog.warning('invalid callback'); | |
| 1142 } | |
| 1143 | |
| 1144 _eventsLog.fine(() => '<<< [$_name]: dispatch $callbackOrMethod'); | |
| 1145 } | |
| 1146 | |
| 1147 /// Call [methodName] method on this object with [args]. | |
| 1148 invokeMethod(Symbol methodName, List args) => | |
| 1149 smoke.invoke(this, methodName, args, adjust: true); | |
| 1150 | |
| 1151 /// Invokes a function asynchronously. | |
| 1152 /// This will call `Polymer.flush()` and then return a `new Timer` | |
| 1153 /// with the provided [method] and [timeout]. | |
| 1154 /// | |
| 1155 /// If you would prefer to run the callback using | |
| 1156 /// [window.requestAnimationFrame], see the [async] method. | |
| 1157 /// | |
| 1158 /// To cancel, call [Timer.cancel] on the result of this method. | |
| 1159 Timer asyncTimer(void method(), Duration timeout) { | |
| 1160 // Dart note: "async" is split into 2 methods so it can have a sensible type | |
| 1161 // signatures. Also removed the various features that don't make sense in a | |
| 1162 // Dart world, like binding to "this" and taking arguments list. | |
| 1163 | |
| 1164 // when polyfilling Object.observe, ensure changes | |
| 1165 // propagate before executing the async method | |
| 1166 scheduleMicrotask(Observable.dirtyCheck); | |
| 1167 _Polymer.callMethod('flush'); // for polymer-js interop | |
| 1168 return new Timer(timeout, method); | |
| 1169 } | |
| 1170 | |
| 1171 /// Invokes a function asynchronously. The context of the callback function is | |
| 1172 /// function is bound to 'this' automatically. Returns a handle which may be | |
| 1173 /// passed to cancelAsync to cancel the asynchronous call. | |
| 1174 /// | |
| 1175 /// If you would prefer to run the callback after a given duration, see | |
| 1176 /// the [asyncTimer] method. | |
| 1177 /// | |
| 1178 /// If you would like to cancel this, use [cancelAsync]. | |
| 1179 int async(RequestAnimationFrameCallback method) { | |
| 1180 // when polyfilling Object.observe, ensure changes | |
| 1181 // propagate before executing the async method | |
| 1182 scheduleMicrotask(Observable.dirtyCheck); | |
| 1183 _Polymer.callMethod('flush'); // for polymer-js interop | |
| 1184 return window.requestAnimationFrame(method); | |
| 1185 } | |
| 1186 | |
| 1187 /// Cancel an operation scheduled by [async]. | |
| 1188 void cancelAsync(int id) => window.cancelAnimationFrame(id); | |
| 1189 | |
| 1190 /// Fire a [CustomEvent] targeting [onNode], or `this` if onNode is not | |
| 1191 /// supplied. Returns the new event. | |
| 1192 CustomEvent fire(String type, {Object detail, Node onNode, bool canBubble, | |
| 1193 bool cancelable}) { | |
| 1194 var node = onNode != null ? onNode : this; | |
| 1195 var event = new CustomEvent( | |
| 1196 type, | |
| 1197 canBubble: canBubble != null ? canBubble : true, | |
| 1198 cancelable: cancelable != null ? cancelable : true, | |
| 1199 detail: detail | |
| 1200 ); | |
| 1201 node.dispatchEvent(event); | |
| 1202 return event; | |
| 1203 } | |
| 1204 | |
| 1205 /// Fire an event asynchronously. See [async] and [fire]. | |
| 1206 asyncFire(String type, {Object detail, Node toNode, bool canBubble}) { | |
| 1207 // TODO(jmesserly): I'm not sure this method adds much in Dart, it's easy to | |
| 1208 // add "() =>" | |
| 1209 async((x) => fire( | |
| 1210 type, detail: detail, onNode: toNode, canBubble: canBubble)); | |
| 1211 } | |
| 1212 | |
| 1213 /// Remove [className] from [old], add class to [anew], if they exist. | |
| 1214 void classFollows(Element anew, Element old, String className) { | |
| 1215 if (old != null) { | |
| 1216 old.classes.remove(className); | |
| 1217 } | |
| 1218 if (anew != null) { | |
| 1219 anew.classes.add(className); | |
| 1220 } | |
| 1221 } | |
| 1222 | |
| 1223 /// Installs external stylesheets and <style> elements with the attribute | |
| 1224 /// polymer-scope='controller' into the scope of element. This is intended | |
| 1225 /// to be called during custom element construction. | |
| 1226 void installControllerStyles() { | |
| 1227 var scope = findStyleScope(); | |
| 1228 if (scope != null && !scopeHasNamedStyle(scope, localName)) { | |
| 1229 // allow inherited controller styles | |
| 1230 var decl = _element; | |
| 1231 var cssText = new StringBuffer(); | |
| 1232 while (decl != null) { | |
| 1233 cssText.write(decl.cssTextForScope(_STYLE_CONTROLLER_SCOPE)); | |
| 1234 decl = decl.superDeclaration; | |
| 1235 } | |
| 1236 if (cssText.isNotEmpty) { | |
| 1237 installScopeCssText('$cssText', scope); | |
| 1238 } | |
| 1239 } | |
| 1240 } | |
| 1241 | |
| 1242 void installScopeStyle(style, [String name, Node scope]) { | |
| 1243 if (scope == null) scope = findStyleScope(); | |
| 1244 if (name == null) name = ''; | |
| 1245 | |
| 1246 if (scope != null && !scopeHasNamedStyle(scope, '$_name$name')) { | |
| 1247 var cssText = new StringBuffer(); | |
| 1248 if (style is Iterable) { | |
| 1249 for (var s in style) { | |
| 1250 cssText..writeln(s.text)..writeln(); | |
| 1251 } | |
| 1252 } else { | |
| 1253 cssText = (style as Node).text; | |
| 1254 } | |
| 1255 installScopeCssText('$cssText', scope, name); | |
| 1256 } | |
| 1257 } | |
| 1258 | |
| 1259 void installScopeCssText(String cssText, [Node scope, String name]) { | |
| 1260 if (scope == null) scope = findStyleScope(); | |
| 1261 if (name == null) name = ''; | |
| 1262 | |
| 1263 if (scope == null) return; | |
| 1264 | |
| 1265 if (_hasShadowDomPolyfill) { | |
| 1266 cssText = _shimCssText(cssText, scope is ShadowRoot ? scope.host : null); | |
| 1267 } | |
| 1268 var style = element.cssTextToScopeStyle(cssText, | |
| 1269 _STYLE_CONTROLLER_SCOPE); | |
| 1270 applyStyleToScope(style, scope); | |
| 1271 // cache that this style has been applied | |
| 1272 styleCacheForScope(scope).add('$_name$name'); | |
| 1273 } | |
| 1274 | |
| 1275 Node findStyleScope([node]) { | |
| 1276 // find the shadow root that contains this element | |
| 1277 var n = node; | |
| 1278 if (n == null) n = this; | |
| 1279 while (n.parentNode != null) { | |
| 1280 n = n.parentNode; | |
| 1281 } | |
| 1282 return n; | |
| 1283 } | |
| 1284 | |
| 1285 bool scopeHasNamedStyle(Node scope, String name) => | |
| 1286 styleCacheForScope(scope).contains(name); | |
| 1287 | |
| 1288 Map _polyfillScopeStyleCache = {}; | |
| 1289 | |
| 1290 Set styleCacheForScope(Node scope) { | |
| 1291 var styles; | |
| 1292 if (_hasShadowDomPolyfill) { | |
| 1293 var name = scope is ShadowRoot ? scope.host.localName | |
| 1294 : (scope as Element).localName; | |
| 1295 var styles = _polyfillScopeStyleCache[name]; | |
| 1296 if (styles == null) _polyfillScopeStyleCache[name] = styles = new Set(); | |
| 1297 } else { | |
| 1298 styles = _scopeStyles[scope]; | |
| 1299 if (styles == null) _scopeStyles[scope] = styles = new Set(); | |
| 1300 } | |
| 1301 return styles; | |
| 1302 } | |
| 1303 | |
| 1304 static final _scopeStyles = new Expando(); | |
| 1305 | |
| 1306 static String _shimCssText(String cssText, [Element host]) { | |
| 1307 var name = ''; | |
| 1308 var is_ = false; | |
| 1309 if (host != null) { | |
| 1310 name = host.localName; | |
| 1311 is_ = host.attributes.containsKey('is'); | |
| 1312 } | |
| 1313 var selector = _ShadowCss.callMethod('makeScopeSelector', [name, is_]); | |
| 1314 return _ShadowCss.callMethod('shimCssText', [cssText, selector]); | |
| 1315 } | |
| 1316 | |
| 1317 static void applyStyleToScope(StyleElement style, Node scope) { | |
| 1318 if (style == null) return; | |
| 1319 | |
| 1320 if (scope == document) scope = document.head; | |
| 1321 | |
| 1322 if (_hasShadowDomPolyfill) scope = document.head; | |
| 1323 | |
| 1324 // TODO(sorvell): necessary for IE | |
| 1325 // see https://connect.microsoft.com/IE/feedback/details/790212/ | |
| 1326 // cloning-a-style-element-and-adding-to-document-produces | |
| 1327 // -unexpected-result#details | |
| 1328 // var clone = style.cloneNode(true); | |
| 1329 var clone = new StyleElement()..text = style.text; | |
| 1330 | |
| 1331 var attr = style.attributes[_STYLE_SCOPE_ATTRIBUTE]; | |
| 1332 if (attr != null) { | |
| 1333 clone.attributes[_STYLE_SCOPE_ATTRIBUTE] = attr; | |
| 1334 } | |
| 1335 | |
| 1336 // TODO(sorvell): probably too brittle; try to figure out | |
| 1337 // where to put the element. | |
| 1338 var refNode = scope.firstChild; | |
| 1339 if (scope == document.head) { | |
| 1340 var selector = 'style[$_STYLE_SCOPE_ATTRIBUTE]'; | |
| 1341 var styleElement = document.head.querySelectorAll(selector); | |
| 1342 if (styleElement.isNotEmpty) { | |
| 1343 refNode = styleElement.last.nextElementSibling; | |
| 1344 } | |
| 1345 } | |
| 1346 scope.insertBefore(clone, refNode); | |
| 1347 } | |
| 1348 | |
| 1349 /// Invoke [callback] in [wait], unless the job is re-registered, | |
| 1350 /// which resets the timer. If [wait] is not supplied, this will use | |
| 1351 /// [window.requestAnimationFrame] instead of a [Timer]. | |
| 1352 /// | |
| 1353 /// For example: | |
| 1354 /// | |
| 1355 /// _myJob = Polymer.scheduleJob(_myJob, callback); | |
| 1356 /// | |
| 1357 /// Returns the newly created job. | |
| 1358 // Dart note: renamed to scheduleJob to be a bit more consistent with Dart. | |
| 1359 PolymerJob scheduleJob(PolymerJob job, void callback(), [Duration wait]) { | |
| 1360 if (job == null) job = new PolymerJob._(); | |
| 1361 // Dart note: made start smarter, so we don't need to call stop. | |
| 1362 return job..start(callback, wait); | |
| 1363 } | |
| 1364 | |
| 1365 /// Inject HTML which contains markup bound to this element into | |
| 1366 /// a target element (replacing target element content). | |
| 1367 DocumentFragment injectBoundHTML(String html, [Element element]) { | |
| 1368 var template = new TemplateElement()..innerHtml = html; | |
| 1369 var fragment = this.instanceTemplate(template); | |
| 1370 if (element != null) { | |
| 1371 element.text = ''; | |
| 1372 element.append(fragment); | |
| 1373 } | |
| 1374 return fragment; | |
| 1375 } | |
| 1376 } | |
| 1377 | |
| 1378 // Dart note: this is related to _bindOldStylePublishedProperty. Polymer | |
| 1379 // addresses n-way bindings by metaprogramming: redefine the property on the | |
| 1380 // PolymerElement instance to always get its value from the model@path. This is | |
| 1381 // supported in Dart using a new style of @published property declaration using | |
| 1382 // the `readValue` and `writeValue` methods above. In the past we used to work | |
| 1383 // around this by listening to changes on both sides and updating the values. | |
| 1384 // This object provides the hooks to do this. | |
| 1385 // TODO(sigmund,jmesserly): delete after a deprecation period. | |
| 1386 class _PolymerBinding extends Bindable { | |
| 1387 final Polymer _target; | |
| 1388 final Symbol _property; | |
| 1389 final Bindable _bindable; | |
| 1390 StreamSubscription _sub; | |
| 1391 Object _lastValue; | |
| 1392 | |
| 1393 _PolymerBinding(this._target, this._property, this._bindable) { | |
| 1394 _sub = _target.changes.listen(_propertyValueChanged); | |
| 1395 _updateNode(open(_updateNode)); | |
| 1396 } | |
| 1397 | |
| 1398 void _updateNode(newValue) { | |
| 1399 _lastValue = newValue; | |
| 1400 smoke.write(_target, _property, newValue); | |
| 1401 // Note: we don't invoke emitPropertyChangeRecord here because that's | |
| 1402 // done by listening on changes on the PolymerElement. | |
| 1403 } | |
| 1404 | |
| 1405 void _propertyValueChanged(List<ChangeRecord> records) { | |
| 1406 for (var record in records) { | |
| 1407 if (record is PropertyChangeRecord && record.name == _property) { | |
| 1408 final newValue = smoke.read(_target, _property); | |
| 1409 if (!identical(_lastValue, newValue)) { | |
| 1410 this.value = newValue; | |
| 1411 } | |
| 1412 return; | |
| 1413 } | |
| 1414 } | |
| 1415 } | |
| 1416 | |
| 1417 open(callback(value)) => _bindable.open(callback); | |
| 1418 get value => _bindable.value; | |
| 1419 set value(newValue) => _bindable.value = newValue; | |
| 1420 | |
| 1421 void close() { | |
| 1422 if (_sub != null) { | |
| 1423 _sub.cancel(); | |
| 1424 _sub = null; | |
| 1425 } | |
| 1426 _bindable.close(); | |
| 1427 } | |
| 1428 } | |
| 1429 | |
| 1430 // Ported from an inline object in instance/properties.js#bindToAccessor. | |
| 1431 class _CloseOnlyBinding extends Bindable { | |
| 1432 final _PropertyAccessor accessor; | |
| 1433 | |
| 1434 _CloseOnlyBinding(this.accessor); | |
| 1435 | |
| 1436 open(callback) {} | |
| 1437 get value => null; | |
| 1438 set value(newValue) {} | |
| 1439 deliver() {} | |
| 1440 | |
| 1441 void close() { | |
| 1442 if (accessor.bindable == null) return; | |
| 1443 accessor.bindable.close(); | |
| 1444 accessor.bindable = null; | |
| 1445 } | |
| 1446 } | |
| 1447 | |
| 1448 bool _toBoolean(value) => null != value && false != value; | |
| 1449 | |
| 1450 final Logger _observeLog = new Logger('polymer.observe'); | |
| 1451 final Logger _eventsLog = new Logger('polymer.events'); | |
| 1452 final Logger _unbindLog = new Logger('polymer.unbind'); | |
| 1453 final Logger _bindLog = new Logger('polymer.bind'); | |
| 1454 final Logger _watchLog = new Logger('polymer.watch'); | |
| 1455 final Logger _readyLog = new Logger('polymer.ready'); | |
| 1456 | |
| 1457 final Expando _eventHandledTable = new Expando<Set<Node>>(); | |
| 1458 | |
| 1459 final JsObject _PolymerGestures = js.context['PolymerGestures']; | |
| 1460 | |
| OLD | NEW |