Chromium Code Reviews| 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 /** | |
| 8 * Use this annotation to publish a field as an attribute. For example: | |
| 9 * | |
| 10 * class MyPlaybackElement extends PolymerElement { | |
| 11 * // This will be available as an HTML attribute, for example: | |
| 12 * // <my-playback volume="11"> | |
| 13 * @published double volume; | |
| 14 * } | |
| 15 */ | |
| 16 // TODO(jmesserly): does @published imply @observable or vice versa? | |
| 17 const published = const PublishedProperty(); | |
| 18 | |
| 19 /** An annotation used to publish a field as an attribute. See [published]. */ | |
| 20 class PublishedProperty extends ObservableProperty { | |
| 21 const PublishedProperty(); | |
| 22 } | |
| 23 | |
| 24 // TODO(jmesserly): make this the mixin so we can have Polymer type extensions, | |
| 25 // and move the implementation of PolymerElement in here. Once done it will look | |
| 26 // like: | |
| 27 // abstract class Polymer { ... all the things ... } | |
| 28 // typedef PolymerElement = HtmlElement with Polymer, Observable; | |
| 29 abstract class Polymer { | |
| 30 // TODO(jmesserly): should this really be public? | |
| 31 /** Regular expression that matches data-bindings. */ | |
| 32 static final bindPattern = new RegExp(r'\{\{([^{}]*)}}'); | |
| 33 | |
| 34 /** | |
| 35 * Like [document.register] but for Polymer elements. | |
| 36 * | |
| 37 * Use the [name] to specify custom elment's tag name, for example: | |
| 38 * "fancy-button" if the tag is used as `<fancy-button>`. | |
| 39 * | |
| 40 * The [type] is the type to construct. If not supplied, it defaults to | |
| 41 * [PolymerElement]. | |
| 42 */ | |
| 43 // NOTE: this is called "element" in src/declaration/polymer-element.js, and | |
| 44 // exported as "Polymer". | |
| 45 static void register(String name, [Type type]) { | |
| 46 //console.log('registering [' + name + ']'); | |
| 47 if (type == null) type = PolymerElement; | |
| 48 _registerClassMirror(name, reflectClass(type)); | |
| 49 } | |
| 50 | |
| 51 // TODO(jmesserly): we use ClassMirror internall for now, until it is possible | |
| 52 // to get from ClassMirror -> Type. | |
| 53 static void _registerClassMirror(String name, ClassMirror type) { | |
| 54 _typesByName[name] = type; | |
| 55 // notify the registrar waiting for 'name', if any | |
| 56 _notifyType(name); | |
| 57 } | |
| 58 } | |
| 59 | |
| 60 /** | |
| 61 * The base class for Polymer elements. It provides convience features on top | |
| 62 * of the custom elements web standard. | |
| 63 */ | |
| 64 class PolymerElement extends CustomElement with ObservableMixin { | |
| 65 // Fully ported from revision: | |
| 66 // https://github.com/Polymer/polymer/blob/4dc481c11505991a7c43228d3797d28f212 67779 | |
| 67 // | |
| 68 // src/instance/attributes.js | |
| 69 // src/instance/base.js | |
| 70 // src/instance/events.js | |
| 71 // src/instance/mdv.js | |
| 72 // src/instance/properties.js | |
| 73 // src/instance/utils.js | |
| 74 // | |
| 75 // Not yet ported: | |
| 76 // src/instance/style.js -- blocked on ShadowCSS.shimPolyfillDirectives | |
| 77 | |
| 78 /// The one syntax to rule them all. | |
| 79 static final BindingDelegate _polymerSyntax = new PolymerExpressions(); | |
| 80 | |
| 81 static int _preparingElements = 0; | |
| 82 | |
| 83 PolymerDeclaration _declaration; | |
| 84 | |
| 85 /** The most derived `<polymer-element>` declaration for this element. */ | |
| 86 PolymerDeclaration get declaration => _declaration; | |
| 87 | |
| 88 Map<String, StreamSubscription> _elementObservers; | |
| 89 bool _unbound; // lazy-initialized | |
| 90 Job _unbindAllJob; | |
| 91 | |
| 92 bool get _elementPrepared => _declaration != null; | |
| 93 | |
| 94 bool get applyAuthorStyles => false; | |
| 95 bool get resetStyleInheritance => false; | |
| 96 bool get alwaysPrepare => false; | |
| 97 | |
| 98 /** | |
| 99 * Shadow roots created by [parseElement]. See [getShadowRoot]. | |
| 100 */ | |
| 101 final _shadowRoots = new HashMap<String, ShadowRoot>(); | |
| 102 | |
| 103 /** Map of items in the shadow root(s) by their [Element.id]. */ | |
| 104 // TODO(jmesserly): various issues: | |
| 105 // * wrap in UnmodifiableMapView? | |
| 106 // * should we have an object that implements noSuchMethod? | |
| 107 // * should the map have a key order (e.g. LinkedHash or SplayTree)? | |
| 108 // * should this be a live list? Polymer doesn't, maybe due to JS limitations? | |
| 109 // For now I picked the most performant choice: non-live HashMap. | |
| 110 final Map<String, Element> $ = new HashMap<String, Element>(); | |
| 111 | |
| 112 /** | |
| 113 * Gets the shadow root associated with the corresponding custom element. | |
| 114 * | |
| 115 * This is identical to [shadowRoot], unless there are multiple levels of | |
| 116 * inheritance and they each have their own shadow root. For example, | |
| 117 * this can happen if the base class and subclass both have `<template>` tags | |
| 118 * in their `<polymer-element>` tags. | |
| 119 */ | |
| 120 // TODO(jmesserly): Polymer does not have this feature. Reconcile. | |
| 121 ShadowRoot getShadowRoot(String customTagName) => _shadowRoots[customTagName]; | |
| 122 | |
| 123 ShadowRoot createShadowRoot([name]) { | |
| 124 if (name != null) { | |
| 125 throw new ArgumentError('name argument must not be supplied.'); | |
| 126 } | |
| 127 | |
| 128 // Provides ability to traverse from ShadowRoot to the host. | |
| 129 // TODO(jmessery): remove once we have this ability on the DOM. | |
| 130 final root = super.createShadowRoot(); | |
| 131 _shadowHost[root] = host; | |
| 132 return root; | |
| 133 } | |
| 134 | |
| 135 /** | |
| 136 * Invoke [callback] in [wait], unless the job is re-registered, | |
| 137 * which resets the timer. For example: | |
| 138 * | |
| 139 * _myJob = job(_myJob, callback, const Duration(milliseconds: 100)); | |
| 140 * | |
| 141 * Returns a job handle which can be used to re-register a job. | |
| 142 */ | |
| 143 Job job(Job job, void callback(), Duration wait) => | |
|
blois
2013/09/27 21:40:52
Why is this needed?
Jennifer Messerly
2013/09/30 17:44:37
agreed, this is a good question for Polymer.js guy
| |
| 144 runJob(job, callback, wait); | |
| 145 | |
| 146 // TODO(jmesserly): I am not sure if we should have the | |
| 147 // created/createdCallback distinction. See post here: | |
| 148 // https://groups.google.com/d/msg/polymer-dev/W0ZUpU5caIM/v5itFnvnehEJ | |
| 149 // Same issue with inserted and removed. | |
| 150 void created() { | |
| 151 if (document.window != null || alwaysPrepare || _preparingElements > 0) { | |
|
blois
2013/09/27 21:40:52
Any idea what the scenarios are where these are cr
Jennifer Messerly
2013/09/30 17:44:37
yeah, it's to workaround <template> upgrading elem
| |
| 152 prepareElement(); | |
| 153 } | |
| 154 } | |
| 155 | |
| 156 void prepareElement() { | |
| 157 // Dart note: get the _declaration, which also marks _elementPrepared | |
| 158 _declaration = _getDeclaration(reflect(this).type); | |
| 159 // do this first so we can observe changes during initialization | |
| 160 observeProperties(); | |
| 161 // install boilerplate attributes | |
| 162 copyInstanceAttributes(); | |
| 163 // process input attributes | |
| 164 takeAttributes(); | |
| 165 // add event listeners | |
| 166 addHostListeners(); | |
| 167 // guarantees that while preparing, any sub-elements will also be prepared | |
| 168 _preparingElements++; | |
| 169 // process declarative resources | |
| 170 parseDeclarations(_declaration); | |
| 171 _preparingElements--; | |
| 172 // user entry point | |
| 173 ready(); | |
| 174 } | |
| 175 | |
| 176 /** Called when [prepareElement] is finished. */ | |
| 177 void ready() {} | |
| 178 | |
| 179 void inserted() { | |
| 180 if (!_elementPrepared) { | |
| 181 prepareElement(); | |
| 182 } | |
| 183 cancelUnbindAll(preventCascade: true); | |
| 184 } | |
| 185 | |
| 186 void removed() { | |
| 187 asyncUnbindAll(); | |
| 188 } | |
| 189 | |
| 190 /** Recursive ancestral <element> initialization, oldest first. */ | |
| 191 void parseDeclarations(PolymerDeclaration declaration) { | |
| 192 if (declaration != null) { | |
| 193 parseDeclarations(declaration.superDeclaration); | |
| 194 parseDeclaration(declaration.host); | |
| 195 } | |
| 196 } | |
| 197 | |
| 198 /** | |
| 199 * Parse input `<polymer-element>` as needed, override for custom behavior. | |
| 200 */ | |
| 201 void parseDeclaration(Element elementElement) { | |
| 202 var root = shadowFromTemplate(fetchTemplate(elementElement)); | |
| 203 | |
| 204 // Dart note: this is extra code compared to Polymer to support | |
| 205 // the getShadowRoot method. | |
| 206 if (root == null) return; | |
| 207 | |
| 208 var name = elementElement.attributes['name']; | |
| 209 if (name == null) return; | |
| 210 _shadowRoots[name] = root; | |
| 211 } | |
| 212 | |
| 213 /** | |
| 214 * Return a shadow-root template (if desired), override for custom behavior. | |
| 215 */ | |
| 216 Element fetchTemplate(Element elementElement) => | |
| 217 elementElement.query('template'); | |
| 218 | |
| 219 /** Utility function that creates a shadow root from a `<template>`. */ | |
| 220 ShadowRoot shadowFromTemplate(Element template) { | |
| 221 if (template == null) return null; | |
| 222 // cache elder shadow root (if any) | |
| 223 var elderRoot = this.shadowRoot; | |
| 224 // make a shadow root | |
| 225 var root = createShadowRoot(); | |
| 226 // migrate flag(s)( | |
| 227 root.applyAuthorStyles = applyAuthorStyles; | |
| 228 root.resetStyleInheritance = resetStyleInheritance; | |
| 229 // stamp template | |
| 230 // which includes parsing and applying MDV bindings before being | |
| 231 // inserted (to avoid {{}} in attribute values) | |
| 232 // e.g. to prevent <img src="images/{{icon}}"> from generating a 404. | |
| 233 var dom = instanceTemplate(template); | |
| 234 // append to shadow dom | |
| 235 root.append(dom); | |
| 236 // perform post-construction initialization tasks on shadow root | |
| 237 shadowRootReady(root, template); | |
| 238 // return the created shadow root | |
| 239 return root; | |
| 240 } | |
| 241 | |
| 242 void shadowRootReady(ShadowRoot root, Element template) { | |
| 243 // locate nodes with id and store references to them in this.$ hash | |
| 244 marshalNodeReferences(root); | |
| 245 // add local events of interest... | |
| 246 addInstanceListeners(root, template); | |
| 247 // TODO(jmesserly): port this | |
| 248 // set up pointer gestures | |
| 249 // PointerGestures.register(root); | |
| 250 } | |
| 251 | |
| 252 /** Locate nodes with id and store references to them in [$] hash. */ | |
| 253 void marshalNodeReferences(ShadowRoot root) { | |
| 254 if (root == null) return; | |
| 255 for (var n in root.queryAll('[id]')) { | |
| 256 $[n.id] = n; | |
| 257 } | |
| 258 } | |
| 259 | |
| 260 void attributeChanged(String name, String oldValue) { | |
|
blois
2013/09/27 21:40:52
FYI- the spec has this as attributeChanged(name, o
Jennifer Messerly
2013/09/30 17:44:37
SGTM. I guess Polymer.js will be dealing with this
| |
| 261 if (name != 'class' && name != 'style') { | |
| 262 attributeToProperty(name, attributes[name]); | |
| 263 } | |
| 264 } | |
| 265 | |
| 266 // TODO(jmesserly): use stream or future here? | |
| 267 void onMutation(Node node, void listener(MutationObserver obs)) { | |
| 268 new MutationObserver((records, MutationObserver observer) { | |
| 269 listener(observer); | |
| 270 observer.disconnect(); | |
| 271 })..observe(node, childList: true, subtree: true); | |
| 272 } | |
| 273 | |
| 274 void copyInstanceAttributes() { | |
| 275 _declaration._instanceAttributes.forEach((name, value) { | |
| 276 attributes[name] = value; | |
| 277 }); | |
| 278 } | |
| 279 | |
| 280 void takeAttributes() { | |
| 281 if (_declaration._publishLC == null) return; | |
| 282 attributes.forEach(attributeToProperty); | |
| 283 } | |
| 284 | |
| 285 /** | |
| 286 * If attribute [name] is mapped to a property, deserialize | |
| 287 * [value] into that property. | |
| 288 */ | |
| 289 void attributeToProperty(String name, String value) { | |
| 290 // try to match this attribute to a property (attributes are | |
| 291 // all lower-case, so this is case-insensitive search) | |
| 292 var property = propertyForAttribute(name); | |
| 293 if (property == null) return; | |
| 294 | |
| 295 // filter out 'mustached' values, these are to be | |
| 296 // replaced with bound-data and are not yet values | |
| 297 // themselves. | |
| 298 if (value == null || value.contains(Polymer.bindPattern)) return; | |
| 299 | |
| 300 // get original value | |
| 301 final self = reflect(this); | |
| 302 final defaultValue = self.getField(property.simpleName).reflectee; | |
| 303 | |
| 304 // deserialize Boolean or Number values from attribute | |
| 305 final newValue = deserializeValue(value, defaultValue, | |
| 306 _inferPropertyType(defaultValue, property)); | |
| 307 | |
| 308 // only act if the value has changed | |
| 309 if (!identical(newValue, defaultValue)) { | |
| 310 // install new value (has side-effects) | |
| 311 self.setField(property.simpleName, newValue); | |
| 312 } | |
| 313 } | |
| 314 | |
| 315 /** Return the published property matching name, or null. */ | |
| 316 // TODO(jmesserly): should we just return Symbol here? | |
| 317 DeclarationMirror propertyForAttribute(String name) { | |
| 318 final publishLC = _declaration._publishLC; | |
| 319 if (publishLC == null) return null; | |
| 320 //console.log('propertyForAttribute:', name, 'matches', match); | |
| 321 return publishLC[name]; | |
| 322 } | |
| 323 | |
| 324 /** | |
| 325 * Convert representation of [value] based on [type] and [defaultValue]. | |
| 326 */ | |
| 327 // TODO(jmesserly): this should probably take a ClassMirror instead of | |
| 328 // TypeMirror, but it is currently impossible to get from a TypeMirror to a | |
| 329 // ClassMirror. | |
| 330 Object deserializeValue(String value, Object defaultValue, TypeMirror type) => | |
| 331 deserialize.deserializeValue(value, defaultValue, type); | |
| 332 | |
| 333 String serializeValue(Object value, TypeMirror inferredType) { | |
| 334 if (value == null) return null; | |
| 335 | |
| 336 final type = inferredType.qualifiedName; | |
| 337 if (type == const Symbol('dart.core.bool')) { | |
| 338 return _toBoolean(value) ? '' : null; | |
| 339 } else if (type == const Symbol('dart.core.String') | |
| 340 || type == const Symbol('dart.core.int') | |
| 341 || type == const Symbol('dart.core.double')) { | |
| 342 return '$value'; | |
| 343 } | |
| 344 return null; | |
| 345 } | |
| 346 | |
| 347 void reflectPropertyToAttribute(String name) { | |
| 348 // TODO(sjmiles): consider memoizing this | |
| 349 final self = reflect(this); | |
| 350 // try to intelligently serialize property value | |
| 351 // TODO(jmesserly): cache symbol? | |
| 352 final propValue = self.getField(new Symbol(name)).reflectee; | |
| 353 final property = _declaration._publish[name]; | |
| 354 var inferredType = _inferPropertyType(propValue, property); | |
| 355 final serializedValue = serializeValue(propValue, inferredType); | |
| 356 // boolean properties must reflect as boolean attributes | |
| 357 if (serializedValue != null) { | |
| 358 attributes[name] = serializedValue; | |
| 359 // TODO(sorvell): we should remove attr for all properties | |
| 360 // that have undefined serialization; however, we will need to | |
| 361 // refine the attr reflection system to achieve this; pica, for example, | |
| 362 // relies on having inferredType object properties not removed as | |
| 363 // attrs. | |
| 364 } else if (inferredType.qualifiedName == const Symbol('dart.core.bool')) { | |
| 365 attributes.remove(name); | |
| 366 } | |
| 367 } | |
| 368 | |
| 369 /** | |
| 370 * Creates the document fragment to use for each instance of the custom | |
| 371 * element, given the `<template>` node. By default this is equivalent to: | |
| 372 * | |
| 373 * template.createInstance(this, polymerSyntax); | |
| 374 * | |
| 375 * Where polymerSyntax is a singleton `PolymerExpressions` instance from the | |
| 376 * [polymer_expressions](https://pub.dartlang.org/packages/polymer_expressions ) | |
| 377 * package. | |
| 378 * | |
| 379 * You can override this method to change the instantiation behavior of the | |
| 380 * template, for example to use a different data-binding syntax. | |
| 381 */ | |
| 382 DocumentFragment instanceTemplate(Element template) => | |
| 383 template.createInstance(this, _polymerSyntax); | |
| 384 | |
| 385 NodeBinding bind(String name, model, String path) { | |
| 386 // note: binding is a prepare signal. This allows us to be sure that any | |
| 387 // property changes that occur as a result of binding will be observed. | |
| 388 if (!_elementPrepared) prepareElement(); | |
| 389 | |
| 390 var property = propertyForAttribute(name); | |
| 391 if (property != null) { | |
| 392 unbind(name); | |
| 393 // use n-way Polymer binding | |
| 394 var observer = bindProperty(property.simpleName, model, path); | |
| 395 // reflect bound property to attribute when binding | |
| 396 // to ensure binding is not left on attribute if property | |
| 397 // does not update due to not changing. | |
| 398 reflectPropertyToAttribute(name); | |
| 399 return bindings[name] = observer; | |
| 400 } else { | |
| 401 return super.bind(name, model, path); | |
| 402 } | |
| 403 } | |
| 404 | |
| 405 void asyncUnbindAll() { | |
| 406 if (_unbound == true) return; | |
| 407 _unbindLog.info('[$localName] asyncUnbindAll'); | |
| 408 _unbindAllJob = job(_unbindAllJob, unbindAll, const Duration(seconds: 0)); | |
| 409 } | |
| 410 | |
| 411 void unbindAll() { | |
| 412 if (_unbound == true) return; | |
| 413 | |
| 414 unbindAllProperties(); | |
| 415 super.unbindAll(); | |
| 416 _unbindNodeTree(shadowRoot); | |
| 417 // TODO(sjmiles): must also unbind inherited shadow roots | |
| 418 _unbound = true; | |
| 419 } | |
| 420 | |
| 421 void cancelUnbindAll({bool preventCascade}) { | |
| 422 if (_unbound == true) { | |
| 423 _unbindLog.warning( | |
| 424 '[$localName] already unbound, cannot cancel unbindAll'); | |
| 425 return; | |
| 426 } | |
| 427 _unbindLog.info('[$localName] cancelUnbindAll'); | |
| 428 if (_unbindAllJob != null) { | |
| 429 _unbindAllJob.stop(); | |
| 430 _unbindAllJob = null; | |
| 431 } | |
| 432 | |
| 433 // cancel unbinding our shadow tree iff we're not in the process of | |
| 434 // cascading our tree (as we do, for example, when the element is inserted). | |
| 435 if (preventCascade == true) return; | |
| 436 _forNodeTree(shadowRoot, (n) { | |
| 437 if (n is PolymerElement) { | |
| 438 (n as PolymerElement).cancelUnbindAll(); | |
| 439 } | |
| 440 }); | |
| 441 } | |
| 442 | |
| 443 static void _unbindNodeTree(Node node) { | |
| 444 _forNodeTree(node, (node) => node.unbindAll()); | |
| 445 } | |
| 446 | |
| 447 static void _forNodeTree(Node node, void callback(Node node)) { | |
| 448 if (node == null) return; | |
| 449 | |
| 450 callback(node); | |
| 451 for (var child = node.firstChild; child != null; child = child.nextNode) { | |
| 452 _forNodeTree(child, callback); | |
| 453 } | |
| 454 } | |
| 455 | |
| 456 /** Set up property observers. */ | |
| 457 void observeProperties() { | |
| 458 // TODO(sjmiles): | |
| 459 // we observe published properties so we can reflect them to attributes | |
| 460 // ~100% of our team's applications would work without this reflection, | |
| 461 // perhaps we can make it optional somehow | |
| 462 // | |
| 463 // add user's observers | |
| 464 final observe = _declaration._observe; | |
| 465 final publish = _declaration._publish; | |
| 466 if (observe != null) { | |
| 467 observe.forEach((name, value) { | |
| 468 if (publish != null && publish.containsKey(name)) { | |
| 469 observeBoth(name, value); | |
| 470 } else { | |
| 471 observeProperty(name, value); | |
| 472 } | |
| 473 }); | |
| 474 } | |
| 475 // add observers for published properties | |
| 476 if (publish != null) { | |
| 477 publish.forEach((name, value) { | |
| 478 if (observe == null || !observe.containsKey(name)) { | |
| 479 observeAttributeProperty(name); | |
| 480 } | |
| 481 }); | |
| 482 } | |
| 483 } | |
| 484 | |
| 485 void _observe(String name, void callback(newValue, oldValue)) { | |
| 486 _observeLog.info('[$localName] watching [$name]'); | |
| 487 // TODO(jmesserly): this is a little different than the JS version so we | |
| 488 // can pass the oldValue, which is missing from Dart's PathObserver. | |
| 489 // This probably gives us worse performance. | |
| 490 var path = new PathObserver(this, name); | |
| 491 Object oldValue = null; | |
| 492 _registerObserver(name, path.changes.listen((_) { | |
| 493 final newValue = path.value; | |
| 494 final old = oldValue; | |
| 495 oldValue = newValue; | |
| 496 callback(newValue, old); | |
| 497 })); | |
| 498 } | |
| 499 | |
| 500 void _registerObserver(String name, StreamSubscription sub) { | |
| 501 if (_elementObservers == null) { | |
| 502 _elementObservers = new Map<String, StreamSubscription>(); | |
| 503 } | |
| 504 _elementObservers[name] = sub; | |
| 505 } | |
| 506 | |
| 507 void observeAttributeProperty(String name) { | |
| 508 _observe(name, (value, old) => reflectPropertyToAttribute(name)); | |
| 509 } | |
| 510 | |
| 511 void observeProperty(String name, Symbol method) { | |
| 512 final self = reflect(this); | |
| 513 _observe(name, (value, old) => self.invoke(method, [old])); | |
| 514 } | |
| 515 | |
| 516 void observeBoth(String name, Symbol methodName) { | |
| 517 final self = reflect(this); | |
| 518 _observe(name, (value, old) { | |
| 519 reflectPropertyToAttribute(name); | |
| 520 self.invoke(methodName, [old]); | |
| 521 }); | |
| 522 } | |
| 523 | |
| 524 void unbindProperty(String name) { | |
| 525 if (_elementObservers == null) return; | |
| 526 var sub = _elementObservers.remove(name); | |
| 527 if (sub != null) sub.cancel(); | |
| 528 } | |
| 529 | |
| 530 void unbindAllProperties() { | |
| 531 if (_elementObservers == null) return; | |
| 532 for (var sub in _elementObservers.values) sub.cancel(); | |
| 533 _elementObservers.clear(); | |
| 534 } | |
| 535 | |
| 536 /** | |
| 537 * Bind a [property] in this object to a [path] in model. *Note* in Dart it | |
| 538 * is necessary to also define the field: | |
| 539 * | |
| 540 * var myProperty; | |
| 541 * | |
| 542 * created() { | |
| 543 * super.created(); | |
| 544 * bindProperty(#myProperty, this, 'myModel.path.to.otherProp'); | |
| 545 * } | |
| 546 */ | |
| 547 // TODO(jmesserly): replace with something more localized, like: | |
| 548 // @ComputedField('myModel.path.to.otherProp'); | |
| 549 NodeBinding bindProperty(Symbol name, Object model, String path) => | |
| 550 // apply Polymer two-way reference binding | |
| 551 _bindProperties(this, name, model, path); | |
| 552 | |
| 553 /** | |
| 554 * bind a property in A to a path in B by converting A[property] to a | |
| 555 * getter/setter pair that accesses B[...path...] | |
| 556 */ | |
| 557 static NodeBinding _bindProperties(PolymerElement inA, Symbol inProperty, | |
| 558 Object inB, String inPath) { | |
| 559 | |
| 560 if (_bindLog.isLoggable(Level.INFO)) { | |
| 561 _bindLog.info('[$inB]: bindProperties: [$inPath] to ' | |
| 562 '[${inA.localName}].[$inProperty]'); | |
| 563 } | |
| 564 | |
| 565 // Dart note: normally we only reach this code when we know it's a | |
| 566 // property, but if someone uses bindProperty directly they might get a | |
| 567 // NoSuchMethodError either from the getField below, or from the setField | |
| 568 // inside PolymerBinding. That doesn't seem unreasonable, but it's a slight | |
| 569 // difference from Polymer.js behavior. | |
| 570 | |
| 571 // capture A's value if B's value is null or undefined, | |
| 572 // otherwise use B's value | |
| 573 var path = new PathObserver(inB, inPath); | |
| 574 if (path.value == null) { | |
| 575 path.value = reflect(inA).getField(inProperty).reflectee; | |
| 576 } | |
| 577 return new _PolymerBinding(inA, inProperty, inB, inPath); | |
| 578 } | |
| 579 | |
| 580 /** Attach event listeners on the host (this) element. */ | |
| 581 void addHostListeners() { | |
| 582 var events = _declaration._eventDelegates; | |
| 583 if (events.isEmpty) return; | |
| 584 | |
| 585 if (_eventsLog.isLoggable(Level.INFO)) { | |
| 586 _eventsLog.info('[$localName] addHostListeners: $events'); | |
| 587 } | |
| 588 addNodeListeners(this, events.keys, hostEventListener); | |
| 589 } | |
| 590 | |
| 591 /** Attach event listeners inside a shadow [root]. */ | |
| 592 void addInstanceListeners(ShadowRoot root, Element template) { | |
| 593 var templateDelegates = _declaration._templateDelegates; | |
| 594 if (templateDelegates == null) return; | |
| 595 var events = templateDelegates[template]; | |
| 596 if (events == null) return; | |
| 597 | |
| 598 if (_eventsLog.isLoggable(Level.INFO)) { | |
| 599 _eventsLog.info('[$localName] addInstanceListeners: $events'); | |
| 600 } | |
| 601 addNodeListeners(root, events, instanceEventListener); | |
| 602 } | |
| 603 | |
| 604 void addNodeListeners(Node node, Iterable<String> events, | |
| 605 void listener(Event e)) { | |
| 606 | |
| 607 for (var name in events) { | |
| 608 addNodeListener(node, name, listener); | |
| 609 } | |
| 610 } | |
| 611 | |
| 612 void addNodeListener(Node node, String event, void listener(Event e)) { | |
| 613 new EventStreamProvider(event).forTarget(node).listen(listener); | |
|
blois
2013/09/27 21:40:52
how about just node.on[event]
Jennifer Messerly
2013/09/30 17:44:37
Doh, I thought it was deprecated; if it's un-depre
| |
| 614 } | |
| 615 | |
| 616 void hostEventListener(Event event) { | |
| 617 // TODO(jmesserly): do we need this check? It was using cancelBubble, see: | |
| 618 // https://github.com/Polymer/polymer/issues/292 | |
| 619 if (!event.bubbles) return; | |
| 620 | |
| 621 bool log = _eventsLog.isLoggable(Level.INFO); | |
| 622 if (log) { | |
| 623 _eventsLog.info('>>> [$localName]: hostEventListener(${event.type})'); | |
| 624 } | |
| 625 | |
| 626 var h = findEventDelegate(event); | |
| 627 if (h) { | |
| 628 if (log) _eventsLog.info('[$localName] found host handler name [$h]'); | |
| 629 var detail = event is CustomEvent ? | |
| 630 (event as CustomEvent).detail : null; | |
| 631 // TODO(jmesserly): cache the symbols? | |
| 632 dispatchMethod(new Symbol(h), [event, detail, this]); | |
| 633 } | |
| 634 | |
| 635 if (log) { | |
| 636 _eventsLog.info('<<< [$localName]: hostEventListener(${event.type})'); | |
| 637 } | |
| 638 } | |
| 639 | |
| 640 String findEventDelegate(Event event) => | |
| 641 _declaration._eventDelegates[_eventNameFromType(event.type)]; | |
| 642 | |
| 643 /** Call [methodName] method on [this] with [args], if the method exists. */ | |
| 644 // TODO(jmesserly): I removed the [node] argument as it was unused. Reconcile. | |
| 645 void dispatchMethod(Symbol methodName, List args) { | |
| 646 bool log = _eventsLog.isLoggable(Level.INFO); | |
| 647 if (log) _eventsLog.info('>>> [$localName]: dispatch $methodName'); | |
| 648 | |
| 649 // TODO(sigmund): consider making event listeners list all arguments | |
| 650 // explicitly. Unless VM mirrors are optimized first, this reflectClass call | |
| 651 // will be expensive once custom elements extend directly from Element (see | |
| 652 // dartbug.com/11108). | |
| 653 var self = reflect(this); | |
| 654 var method = self.type.methods[methodName]; | |
| 655 if (method != null) { | |
| 656 // This will either truncate the argument list or extend it with extra | |
| 657 // null arguments, so it will match the signature. | |
| 658 // TODO(sigmund): consider accepting optional arguments when we can tell | |
| 659 // them appart from named arguments (see http://dartbug.com/11334) | |
| 660 args.length = method.parameters.where((p) => !p.isOptional).length; | |
| 661 } | |
| 662 self.invoke(methodName, args); | |
| 663 | |
| 664 if (log) _eventsLog.info('<<< [$localName]: dispatch $methodName'); | |
| 665 | |
| 666 // TODO(jmesserly): workaround for HTML events not supporting zones. | |
| 667 performMicrotaskCheckpoint(); | |
| 668 } | |
| 669 | |
| 670 void instanceEventListener(Event event) { | |
| 671 _listenLocal(host, event); | |
| 672 } | |
| 673 | |
| 674 // TODO(sjmiles): much of the below privatized only because of the vague | |
| 675 // notion this code is too fiddly and we need to revisit the core feature | |
| 676 void _listenLocal(Element host, Event event) { | |
| 677 // TODO(jmesserly): do we need this check? It was using cancelBubble, see: | |
| 678 // https://github.com/Polymer/polymer/issues/292 | |
| 679 if (!event.bubbles) return; | |
| 680 | |
| 681 bool log = _eventsLog.isLoggable(Level.INFO); | |
| 682 if (log) _eventsLog.info('>>> [$localName]: listenLocal [${event.type}]'); | |
| 683 | |
| 684 final eventOn = '$_EVENT_PREFIX${_eventNameFromType(event.type)}'; | |
| 685 if (event.path == null) { | |
| 686 _listenLocalNoEventPath(host, event, eventOn); | |
| 687 } else { | |
| 688 _listenLocalEventPath(host, event, eventOn); | |
| 689 } | |
| 690 | |
| 691 if (log) _eventsLog.info('<<< [$localName]: listenLocal [${event.type}]'); | |
| 692 } | |
| 693 | |
| 694 static void _listenLocalEventPath(Element host, Event event, String eventOn) { | |
| 695 var c = null; | |
| 696 for (var target in event.path) { | |
| 697 // if we hit host, stop | |
| 698 if (identical(target, host)) return; | |
| 699 | |
| 700 // find a controller for the target, unless we already found `host` | |
| 701 // as a controller | |
| 702 c = identical(c, host) ? c : _findController(target); | |
| 703 | |
| 704 // if we have a controller, dispatch the event, and stop if the handler | |
| 705 // returns true | |
| 706 if (c != null && _handleEvent(c, target, event, eventOn)) { | |
| 707 return; | |
| 708 } | |
| 709 } | |
| 710 } | |
| 711 | |
| 712 // TODO(sorvell): remove when ShadowDOM polyfill supports event path. | |
| 713 // Note that _findController will not return the expected controller when the | |
| 714 // event target is a distributed node. This is because we cannot traverse | |
| 715 // from a composed node to a node in shadowRoot. | |
| 716 // This will be addressed via an event path api | |
| 717 // https://www.w3.org/Bugs/Public/show_bug.cgi?id=21066 | |
| 718 static void _listenLocalNoEventPath(Element host, Event event, | |
| 719 String eventOn) { | |
| 720 | |
| 721 if (_eventsLog.isLoggable(Level.INFO)) { | |
| 722 _eventsLog.info('event.path() not supported for ${event.type}'); | |
| 723 } | |
| 724 | |
| 725 var target = event.target; | |
| 726 var c = null; | |
| 727 // if we hit dirt or host, stop | |
| 728 while (target != null && target != host) { | |
| 729 // find a controller for target `t`, unless we already found `host` | |
| 730 // as a controller | |
| 731 c = identical(c, host) ? c : _findController(target); | |
| 732 | |
| 733 // if we have a controller, dispatch the event, return 'true' if | |
| 734 // handler returns true | |
| 735 if (c != null && _handleEvent(c, target, event, eventOn)) { | |
| 736 return; | |
| 737 } | |
| 738 target = target.parent; | |
| 739 } | |
| 740 } | |
| 741 | |
| 742 // TODO(jmesserly): this won't find the correct host unless the ShadowRoot | |
| 743 // was created on a PolymerElement. | |
| 744 static Element _findController(Node node) { | |
| 745 while (node.parentNode != null) { | |
| 746 node = node.parentNode; | |
| 747 } | |
| 748 return _shadowHost[node]; | |
| 749 } | |
| 750 | |
| 751 static bool _handleEvent(Element ctrlr, Node node, Event event, | |
| 752 String eventOn) { | |
| 753 | |
| 754 // Note: local events are listened only in the shadow root. This dynamic | |
| 755 // lookup is used to distinguish determine whether the target actually has a | |
| 756 // listener, and if so, to determine lazily what's the target method. | |
| 757 var name = node is Element ? (node as Element).attributes[eventOn] : null; | |
| 758 if (name != null && _handleIfNotHandled(node, event)) { | |
| 759 if (_eventsLog.isLoggable(Level.INFO)) { | |
| 760 _eventsLog.info('[${ctrlr.localName}] found handler name [$name]'); | |
| 761 } | |
| 762 var detail = event is CustomEvent ? | |
| 763 (event as CustomEvent).detail : null; | |
| 764 | |
| 765 if (node != null) { | |
| 766 // TODO(jmesserly): cache symbols? | |
| 767 ctrlr.xtag.dispatchMethod(new Symbol(name), [event, detail, node]); | |
| 768 } | |
| 769 } | |
| 770 return event.cancelBubble; | |
| 771 } | |
| 772 | |
| 773 // TODO(jmesserly): I don't understand this bit. It seems to be a duplicate | |
| 774 // delivery prevention mechanism? | |
| 775 static bool _handleIfNotHandled(Node node, Event event) { | |
| 776 var list = _eventHandledTable[event]; | |
| 777 if (list == null) _eventHandledTable[event] = list = new Set<Node>(); | |
| 778 if (!list.contains(node)) { | |
| 779 list.add(node); | |
| 780 return true; | |
| 781 } | |
| 782 return false; | |
| 783 } | |
| 784 | |
| 785 /** | |
| 786 * Invokes a function asynchronously. | |
| 787 * This will call `Platform.flush()` and then return a `new Timer` | |
| 788 * with the provided [method] and [timeout]. | |
| 789 * | |
| 790 * If you would prefer to run the callback using | |
| 791 * [window.requestAnimationFrame], see the [async] method. | |
| 792 */ | |
| 793 // Dart note: "async" is split into 2 methods so it can have a sensible type | |
| 794 // signatures. Also removed the various features that don't make sense in a | |
| 795 // Dart world, like binding to "this" and taking arguments list. | |
| 796 Timer asyncTimer(void method(), Duration timeout) { | |
| 797 // when polyfilling Object.observe, ensure changes | |
| 798 // propagate before executing the async method | |
| 799 platform.flush(); | |
| 800 return new Timer(timeout, method); | |
|
blois
2013/09/27 21:40:52
Why is the flush performed during invoke and not a
Jennifer Messerly
2013/09/30 17:44:37
Agree, added to #13666
| |
| 801 } | |
| 802 | |
| 803 /** | |
| 804 * Invokes a function asynchronously. | |
| 805 * This will call `Platform.flush()` and then call | |
| 806 * [window.requestAnimationFrame] with the provided [method] and return the | |
| 807 * result. | |
| 808 * | |
| 809 * If you would prefer to run the callback after a given duration, see | |
| 810 * the [asyncTimer] method. | |
| 811 */ | |
| 812 int async(RequestAnimationFrameCallback method) { | |
| 813 // when polyfilling Object.observe, ensure changes | |
| 814 // propagate before executing the async method | |
| 815 platform.flush(); | |
| 816 return window.requestAnimationFrame(method); | |
| 817 } | |
| 818 | |
| 819 /** | |
| 820 * Fire a [CustomEvent] targeting [toNode], or this if toNode is not | |
| 821 * supplied. Returns the [detail] object. | |
| 822 */ | |
| 823 Object fire(String type, {Object detail, Node toNode, bool canBubble}) { | |
| 824 var node = toNode != null ? toNode : this; | |
| 825 //log.events && console.log('[%s]: sending [%s]', node.localName, inType); | |
| 826 node.dispatchEvent(new CustomEvent( | |
| 827 type, | |
| 828 canBubble: canBubble != null ? canBubble : true, | |
| 829 detail: detail | |
| 830 )); | |
| 831 return detail; | |
| 832 } | |
| 833 | |
| 834 /** | |
| 835 * Fire an event asynchronously. See [async] and [fire]. | |
| 836 */ | |
| 837 asyncFire(String type, {Object detail, Node toNode, bool canBubble}) { | |
| 838 // TODO(jmesserly): I'm not sure this method adds much in Dart, it's easy to | |
| 839 // add "() =>" | |
| 840 async((x) => fire( | |
| 841 type, detail: detail, toNode: toNode, canBubble: canBubble)); | |
| 842 } | |
| 843 | |
| 844 /** | |
| 845 * Remove [className] from [old], add class to [anew], if they exist. | |
| 846 */ | |
| 847 void classFollows(Element anew, Element old, String className) { | |
| 848 if (old != null) { | |
| 849 old.classes.remove(className); | |
| 850 } | |
| 851 if (anew != null) { | |
| 852 anew.classes.add(className); | |
| 853 } | |
| 854 } | |
| 855 } | |
| 856 | |
| 857 // Dart note: Polymer addresses n-way bindings by metaprogramming: redefine | |
| 858 // the property on the PolymerElement instance to always get its value from the | |
| 859 // model@path. We can't replicate this in Dart so we do the next best thing: | |
| 860 // listen to changes on both sides and update the values. | |
| 861 // TODO(jmesserly): our approach leads to race conditions in the bindings. | |
| 862 // See http://code.google.com/p/dart/issues/detail?id=13567 | |
| 863 class _PolymerBinding extends NodeBinding { | |
| 864 final InstanceMirror _target; | |
| 865 final Symbol _property; | |
| 866 StreamSubscription _sub; | |
| 867 Object _lastValue; | |
| 868 | |
| 869 _PolymerBinding(PolymerElement node, Symbol property, model, path) | |
| 870 : _target = reflect(node), | |
| 871 _property = property, | |
| 872 super(node, MirrorSystem.getName(property), model, path) { | |
| 873 | |
| 874 _sub = node.changes.listen(_propertyValueChanged); | |
| 875 } | |
| 876 | |
| 877 void close() { | |
| 878 if (closed) return; | |
| 879 _sub.cancel(); | |
| 880 super.close(); | |
| 881 } | |
| 882 | |
| 883 void boundValueChanged(newValue) { | |
| 884 _lastValue = newValue; | |
| 885 _target.setField(_property, newValue); | |
| 886 } | |
| 887 | |
| 888 void _propertyValueChanged(List<ChangeRecord> records) { | |
| 889 for (var record in records) { | |
| 890 if (record.changes(_property)) { | |
| 891 final newValue = _target.getField(_property).reflectee; | |
| 892 if (!identical(_lastValue, newValue)) { | |
| 893 value = newValue; | |
| 894 } | |
| 895 return; | |
| 896 } | |
| 897 } | |
| 898 } | |
| 899 } | |
| 900 | |
| 901 bool _toBoolean(value) => null != value && false != value; | |
| 902 | |
| 903 TypeMirror _propertyType(DeclarationMirror property) => | |
| 904 property is VariableMirror | |
| 905 ? (property as VariableMirror).type | |
| 906 : (property as MethodMirror).returnType; | |
| 907 | |
| 908 TypeMirror _inferPropertyType(Object value, DeclarationMirror property) { | |
| 909 var type = _propertyType(property); | |
| 910 if (type.qualifiedName == const Symbol('dart.core.Object') || | |
| 911 type.qualifiedName == const Symbol('dynamic')) { | |
| 912 // Attempt to infer field type from the default value. | |
| 913 if (value != null) { | |
| 914 type = reflect(value).type; | |
| 915 } | |
| 916 } | |
| 917 return type; | |
| 918 } | |
| 919 | |
| 920 final Logger _observeLog = new Logger('polymer.observe'); | |
| 921 final Logger _eventsLog = new Logger('polymer.events'); | |
| 922 final Logger _unbindLog = new Logger('polymer.unbind'); | |
| 923 final Logger _bindLog = new Logger('polymer.bind'); | |
| 924 | |
| 925 final Expando _shadowHost = new Expando<Element>(); | |
| 926 | |
| 927 final Expando _eventHandledTable = new Expando<Set<Node>>(); | |
| OLD | NEW |