| OLD | NEW |
| (Empty) | |
| 1 <!-- |
| 2 @license |
| 3 Copyright (c) 2017 The Polymer Project Authors. All rights reserved. |
| 4 This code may only be used under the BSD style license found at http://polymer.g
ithub.io/LICENSE.txt |
| 5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| 6 The complete set of contributors may be found at http://polymer.github.io/CONTRI
BUTORS.txt |
| 7 Code distributed by Google as part of the polymer project is also |
| 8 subject to an additional IP rights grant found at http://polymer.github.io/PATEN
TS.txt |
| 9 --> |
| 10 |
| 11 <link rel="import" href="../utils/boot.html"> |
| 12 <link rel="import" href="../utils/mixin.html"> |
| 13 <link rel="import" href="../utils/case-map.html"> |
| 14 <link rel="import" href="../utils/async.html"> |
| 15 |
| 16 <script> |
| 17 (function() { |
| 18 |
| 19 'use strict'; |
| 20 |
| 21 let caseMap = Polymer.CaseMap; |
| 22 |
| 23 let microtask = Polymer.Async.microTask; |
| 24 |
| 25 // Save map of native properties; this forms a blacklist or properties |
| 26 // that won't have their values "saved" by `saveAccessorValue`, since |
| 27 // reading from an HTMLElement accessor from the context of a prototype throws |
| 28 const nativeProperties = {}; |
| 29 let proto = HTMLElement.prototype; |
| 30 while (proto) { |
| 31 let props = Object.getOwnPropertyNames(proto); |
| 32 for (let i=0; i<props.length; i++) { |
| 33 nativeProperties[props[i]] = true; |
| 34 } |
| 35 proto = Object.getPrototypeOf(proto); |
| 36 } |
| 37 |
| 38 /** |
| 39 * Used to save the value of a property that will be overridden with |
| 40 * an accessor. If the `model` is a prototype, the values will be saved |
| 41 * in `__dataProto`, and it's up to the user (or downstream mixin) to |
| 42 * decide how/when to set these values back into the accessors. |
| 43 * If `model` is already an instance (it has a `__data` property), then |
| 44 * the value will be set as a pending property, meaning the user should |
| 45 * call `_invalidateProperties` or `_flushProperties` to take effect |
| 46 * |
| 47 * @param {Object} model Prototype or instance |
| 48 * @param {string} property Name of property |
| 49 * @private |
| 50 */ |
| 51 function saveAccessorValue(model, property) { |
| 52 // Don't read/store value for any native properties since they could throw |
| 53 if (!nativeProperties[property]) { |
| 54 let value = model[property]; |
| 55 if (value !== undefined) { |
| 56 if (model.__data) { |
| 57 // Adding accessor to instance; update the property |
| 58 // It is the user's responsibility to call _flushProperties |
| 59 model._setPendingProperty(property, value); |
| 60 } else { |
| 61 // Adding accessor to proto; save proto's value for instance-time use |
| 62 if (!model.__dataProto) { |
| 63 model.__dataProto = {}; |
| 64 } else if (!model.hasOwnProperty(JSCompiler_renameProperty('__dataProt
o', model))) { |
| 65 model.__dataProto = Object.create(model.__dataProto); |
| 66 } |
| 67 model.__dataProto[property] = value; |
| 68 } |
| 69 } |
| 70 } |
| 71 } |
| 72 |
| 73 /** |
| 74 * Element class mixin that provides basic meta-programming for creating one |
| 75 * or more property accessors (getter/setter pair) that enqueue an async |
| 76 * (batched) `_propertiesChanged` callback. |
| 77 * |
| 78 * For basic usage of this mixin, simply declare attributes to observe via |
| 79 * the standard `static get observedAttributes()`, implement `_propertiesChang
ed` |
| 80 * on the class, and then call `MyClass.createPropertiesForAttributes()` once |
| 81 * on the class to generate property accessors for each observed attribute |
| 82 * prior to instancing. Last, call `this._flushProperties()` once to enable |
| 83 * the accessors. |
| 84 * |
| 85 * Any `observedAttributes` will automatically be |
| 86 * deserialized via `attributeChangedCallback` and set to the associated |
| 87 * property using `dash-case`-to-`camelCase` convention. |
| 88 * |
| 89 * @polymerMixin |
| 90 * @memberof Polymer |
| 91 * @summary Element class mixin for reacting to property changes from |
| 92 * generated property accessors. |
| 93 */ |
| 94 Polymer.PropertyAccessors = Polymer.dedupingMixin(superClass => { |
| 95 |
| 96 /** |
| 97 * @polymerMixinClass |
| 98 * @implements {Polymer_PropertyAccessors} |
| 99 * @unrestricted |
| 100 */ |
| 101 class PropertyAccessors extends superClass { |
| 102 |
| 103 /** |
| 104 * Generates property accessors for all attributes in the standard |
| 105 * static `observedAttributes` array. |
| 106 * |
| 107 * Attribute names are mapped to property names using the `dash-case` to |
| 108 * `camelCase` convention |
| 109 * |
| 110 */ |
| 111 static createPropertiesForAttributes() { |
| 112 let a$ = this.observedAttributes; |
| 113 for (let i=0; i < a$.length; i++) { |
| 114 this.prototype._createPropertyAccessor(caseMap.dashToCamelCase(a$[i]))
; |
| 115 } |
| 116 } |
| 117 |
| 118 constructor() { |
| 119 super(); |
| 120 this._initializeProperties(); |
| 121 } |
| 122 |
| 123 attributeChangedCallback(name, old, value) { |
| 124 if (old !== value) { |
| 125 this._attributeToProperty(name, value); |
| 126 } |
| 127 } |
| 128 |
| 129 /** |
| 130 * Initializes the local storage for property accessors. |
| 131 * |
| 132 * Provided as an override point for performing any setup work prior |
| 133 * to initializing the property accessor system. |
| 134 * |
| 135 * @protected |
| 136 */ |
| 137 _initializeProperties() { |
| 138 this.__serializing = false; |
| 139 this.__dataCounter = 0; |
| 140 this.__dataEnabled = false; |
| 141 this.__dataReady = false; |
| 142 this.__dataInvalid = false; |
| 143 // initialize data with prototype values saved when creating accessors |
| 144 this.__data = {}; |
| 145 this.__dataPending = null; |
| 146 this.__dataOld = null; |
| 147 if (this.__dataProto) { |
| 148 this._initializeProtoProperties(this.__dataProto); |
| 149 this.__dataProto = null; |
| 150 } |
| 151 // Capture instance properties; these will be set into accessors |
| 152 // during first flush. Don't set them here, since we want |
| 153 // these to overwrite defaults/constructor assignments |
| 154 for (let p in this.__dataHasAccessor) { |
| 155 if (this.hasOwnProperty(p)) { |
| 156 this.__dataInstanceProps = this.__dataInstanceProps || {}; |
| 157 this.__dataInstanceProps[p] = this[p]; |
| 158 delete this[p]; |
| 159 } |
| 160 } |
| 161 } |
| 162 |
| 163 /** |
| 164 * Called at instance time with bag of properties that were overwritten |
| 165 * by accessors on the prototype when accessors were created. |
| 166 * |
| 167 * The default implementation sets these properties back into the |
| 168 * setter at instance time. This method is provided as an override |
| 169 * point for customizing or providing more efficient initialization. |
| 170 * |
| 171 * @param {Object} props Bag of property values that were overwritten |
| 172 * when creating property accessors. |
| 173 * @protected |
| 174 */ |
| 175 _initializeProtoProperties(props) { |
| 176 for (let p in props) { |
| 177 this._setProperty(p, props[p]); |
| 178 } |
| 179 } |
| 180 |
| 181 /** |
| 182 * Called at ready time with bag of instance properties that overwrote |
| 183 * accessors when the element upgraded. |
| 184 * |
| 185 * The default implementation sets these properties back into the |
| 186 * setter at ready time. This method is provided as an override |
| 187 * point for customizing or providing more efficient initialization. |
| 188 * |
| 189 * @param {Object} props Bag of property values that were overwritten |
| 190 * when creating property accessors. |
| 191 * @protected |
| 192 */ |
| 193 _initializeInstanceProperties(props) { |
| 194 Object.assign(this, props); |
| 195 } |
| 196 |
| 197 /** |
| 198 * Ensures the element has the given attribute. If it does not, |
| 199 * assigns the given value to the attribute. |
| 200 * |
| 201 * |
| 202 * @param {string} attribute Name of attribute to ensure is set. |
| 203 * @param {string} value of the attribute. |
| 204 */ |
| 205 _ensureAttribute(attribute, value) { |
| 206 if (!this.hasAttribute(attribute)) { |
| 207 this._valueToNodeAttribute(this, value, attribute); |
| 208 } |
| 209 } |
| 210 |
| 211 /** |
| 212 * Deserializes an attribute to its associated property. |
| 213 * |
| 214 * This method calls the `_deserializeValue` method to convert the string
to |
| 215 * a typed value. |
| 216 * |
| 217 * @param {string} attribute Name of attribute to deserialize. |
| 218 * @param {string} value of the attribute. |
| 219 * @param {*} type type to deserialize to. |
| 220 */ |
| 221 _attributeToProperty(attribute, value, type) { |
| 222 // Don't deserialize back to property if currently reflecting |
| 223 if (!this.__serializing) { |
| 224 let property = caseMap.dashToCamelCase(attribute); |
| 225 this[property] = this._deserializeValue(value, type); |
| 226 } |
| 227 } |
| 228 |
| 229 /** |
| 230 * Serializes a property to its associated attribute. |
| 231 * |
| 232 * @param {string} property Property name to reflect. |
| 233 * @param {string=} attribute Attribute name to reflect. |
| 234 * @param {*=} value Property value to refect. |
| 235 */ |
| 236 _propertyToAttribute(property, attribute, value) { |
| 237 this.__serializing = true; |
| 238 value = (arguments.length < 3) ? this[property] : value; |
| 239 this._valueToNodeAttribute(this, value, |
| 240 attribute || caseMap.camelToDashCase(property)); |
| 241 this.__serializing = false; |
| 242 } |
| 243 |
| 244 /** |
| 245 * Sets a typed value to an HTML attribute on a node. |
| 246 * |
| 247 * This method calls the `_serializeValue` method to convert the typed |
| 248 * value to a string. If the `_serializeValue` method returns `undefined`
, |
| 249 * the attribute will be removed (this is the default for boolean |
| 250 * type `false`). |
| 251 * |
| 252 * @param {Element} node Element to set attribute to. |
| 253 * @param {*} value Value to serialize. |
| 254 * @param {string} attribute Attribute name to serialize to. |
| 255 */ |
| 256 _valueToNodeAttribute(node, value, attribute) { |
| 257 let str = this._serializeValue(value); |
| 258 if (str === undefined) { |
| 259 node.removeAttribute(attribute); |
| 260 } else { |
| 261 node.setAttribute(attribute, str); |
| 262 } |
| 263 } |
| 264 |
| 265 /** |
| 266 * Converts a typed JavaScript value to a string. |
| 267 * |
| 268 * This method is called by Polymer when setting JS property values to |
| 269 * HTML attributes. Users may override this method on Polymer element |
| 270 * prototypes to provide serialization for custom types. |
| 271 * |
| 272 * @param {*} value Property value to serialize. |
| 273 * @return {string | undefined} String serialized from the provided proper
ty value. |
| 274 */ |
| 275 _serializeValue(value) { |
| 276 /* eslint-disable no-fallthrough */ |
| 277 switch (typeof value) { |
| 278 case 'boolean': |
| 279 return value ? '' : undefined; |
| 280 |
| 281 case 'object': |
| 282 if (value instanceof Date) { |
| 283 return value.toString(); |
| 284 } else if (value) { |
| 285 try { |
| 286 return JSON.stringify(value); |
| 287 } catch(x) { |
| 288 return ''; |
| 289 } |
| 290 } |
| 291 |
| 292 default: |
| 293 return value != null ? value.toString() : undefined; |
| 294 } |
| 295 } |
| 296 |
| 297 /** |
| 298 * Converts a string to a typed JavaScript value. |
| 299 * |
| 300 * This method is called by Polymer when reading HTML attribute values to |
| 301 * JS properties. Users may override this method on Polymer element |
| 302 * prototypes to provide deserialization for custom `type`s. Note, |
| 303 * the `type` argument is the value of the `type` field provided in the |
| 304 * `properties` configuration object for a given property, and is |
| 305 * by convention the constructor for the type to deserialize. |
| 306 * |
| 307 * Note: The return value of `undefined` is used as a sentinel value to |
| 308 * indicate the attribute should be removed. |
| 309 * |
| 310 * @param {string} value Attribute value to deserialize. |
| 311 * @param {*} type Type to deserialize the string to. |
| 312 * @return {*} Typed value deserialized from the provided string. |
| 313 */ |
| 314 _deserializeValue(value, type) { |
| 315 /** |
| 316 * @type {*} |
| 317 */ |
| 318 let outValue; |
| 319 switch (type) { |
| 320 case Number: |
| 321 outValue = Number(value); |
| 322 break; |
| 323 |
| 324 case Boolean: |
| 325 outValue = (value !== null); |
| 326 break; |
| 327 |
| 328 case Object: |
| 329 try { |
| 330 outValue = JSON.parse(value); |
| 331 } catch(x) { |
| 332 // allow non-JSON literals like Strings and Numbers |
| 333 } |
| 334 break; |
| 335 |
| 336 case Array: |
| 337 try { |
| 338 outValue = JSON.parse(value); |
| 339 } catch(x) { |
| 340 outValue = null; |
| 341 console.warn(`Polymer::Attributes: couldn't decode Array as JSON:
${value}`); |
| 342 } |
| 343 break; |
| 344 |
| 345 case Date: |
| 346 outValue = new Date(value); |
| 347 break; |
| 348 |
| 349 case String: |
| 350 default: |
| 351 outValue = value; |
| 352 break; |
| 353 } |
| 354 |
| 355 return outValue; |
| 356 } |
| 357 /* eslint-enable no-fallthrough */ |
| 358 |
| 359 /** |
| 360 * Creates a setter/getter pair for the named property with its own |
| 361 * local storage. The getter returns the value in the local storage, |
| 362 * and the setter calls `_setProperty`, which updates the local storage |
| 363 * for the property and enqueues a `_propertiesChanged` callback. |
| 364 * |
| 365 * This method may be called on a prototype or an instance. Calling |
| 366 * this method may overwrite a property value that already exists on |
| 367 * the prototype/instance by creating the accessor. When calling on |
| 368 * a prototype, any overwritten values are saved in `__dataProto`, |
| 369 * and it is up to the subclasser to decide how/when to set those |
| 370 * properties back into the accessor. When calling on an instance, |
| 371 * the overwritten value is set via `_setPendingProperty`, and the |
| 372 * user should call `_invalidateProperties` or `_flushProperties` |
| 373 * for the values to take effect. |
| 374 * |
| 375 * @param {string} property Name of the property |
| 376 * @param {boolean=} readOnly When true, no setter is created; the |
| 377 * protected `_setProperty` function must be used to set the property |
| 378 * @protected |
| 379 */ |
| 380 _createPropertyAccessor(property, readOnly) { |
| 381 if (!this.hasOwnProperty('__dataHasAccessor')) { |
| 382 this.__dataHasAccessor = Object.assign({}, this.__dataHasAccessor); |
| 383 } |
| 384 if (!this.__dataHasAccessor[property]) { |
| 385 this.__dataHasAccessor[property] = true; |
| 386 saveAccessorValue(this, property); |
| 387 Object.defineProperty(this, property, { |
| 388 get: function() { |
| 389 return this.__data[property]; |
| 390 }, |
| 391 set: readOnly ? function() { } : function(value) { |
| 392 this._setProperty(property, value); |
| 393 } |
| 394 }); |
| 395 } |
| 396 } |
| 397 |
| 398 /** |
| 399 * Returns true if this library created an accessor for the given property
. |
| 400 * |
| 401 * @param {string} property Property name |
| 402 * @return {boolean} True if an accessor was created |
| 403 */ |
| 404 _hasAccessor(property) { |
| 405 return this.__dataHasAccessor && this.__dataHasAccessor[property]; |
| 406 } |
| 407 |
| 408 /** |
| 409 * Updates the local storage for a property (via `_setPendingProperty`) |
| 410 * and enqueues a `_proeprtiesChanged` callback. |
| 411 * |
| 412 * @param {string} property Name of the property |
| 413 * @param {*} value Value to set |
| 414 * @protected |
| 415 */ |
| 416 _setProperty(property, value) { |
| 417 if (this._setPendingProperty(property, value)) { |
| 418 this._invalidateProperties(); |
| 419 } |
| 420 } |
| 421 |
| 422 /** |
| 423 * Updates the local storage for a property, records the previous value, |
| 424 * and adds it to the set of "pending changes" that will be passed to the |
| 425 * `_propertiesChanged` callback. This method does not enqueue the |
| 426 * `_propertiesChanged` callback. |
| 427 * |
| 428 * @param {string} property Name of the property |
| 429 * @param {*} value Value to set |
| 430 * @return {boolean} Returns true if the property changed |
| 431 * @protected |
| 432 */ |
| 433 _setPendingProperty(property, value) { |
| 434 let old = this.__data[property]; |
| 435 if (this._shouldPropertyChange(property, value, old)) { |
| 436 if (!this.__dataPending) { |
| 437 this.__dataPending = {}; |
| 438 this.__dataOld = {}; |
| 439 } |
| 440 // Ensure old is captured from the last turn |
| 441 if (!(property in this.__dataOld)) { |
| 442 this.__dataOld[property] = old; |
| 443 } |
| 444 this.__data[property] = value; |
| 445 this.__dataPending[property] = value; |
| 446 return true; |
| 447 } |
| 448 } |
| 449 |
| 450 /** |
| 451 * Returns true if the specified property has a pending change. |
| 452 * |
| 453 * @param {string} prop Property name |
| 454 * @return {boolean} True if property has a pending change |
| 455 * @protected |
| 456 */ |
| 457 _isPropertyPending(prop) { |
| 458 return this.__dataPending && (prop in this.__dataPending); |
| 459 } |
| 460 |
| 461 /** |
| 462 * Marks the properties as invalid, and enqueues an async |
| 463 * `_propertiesChanged` callback. |
| 464 * |
| 465 * @protected |
| 466 */ |
| 467 _invalidateProperties() { |
| 468 if (!this.__dataInvalid && this.__dataReady) { |
| 469 this.__dataInvalid = true; |
| 470 microtask.run(() => { |
| 471 if (this.__dataInvalid) { |
| 472 this.__dataInvalid = false; |
| 473 this._flushProperties(); |
| 474 } |
| 475 }); |
| 476 } |
| 477 } |
| 478 |
| 479 /** |
| 480 * Call to enable property accessor processing. Before this method is |
| 481 * called accessor values will be set but side effects are |
| 482 * queued. When called, any pending side effects occur immediately. |
| 483 * For elements, generally `connectedCallback` is a normal spot to do so. |
| 484 * It is safe to call this method multiple times as it only turns on |
| 485 * property accessors once. |
| 486 */ |
| 487 _enableProperties() { |
| 488 if (!this.__dataEnabled) { |
| 489 this.__dataEnabled = true; |
| 490 if (this.__dataInstanceProps) { |
| 491 this._initializeInstanceProperties(this.__dataInstanceProps); |
| 492 this.__dataInstanceProps = null; |
| 493 } |
| 494 this.ready() |
| 495 } |
| 496 } |
| 497 |
| 498 /** |
| 499 * Calls the `_propertiesChanged` callback with the current set of |
| 500 * pending changes (and old values recorded when pending changes were |
| 501 * set), and resets the pending set of changes. Generally, this method |
| 502 * should not be called in user code. |
| 503 * |
| 504 * |
| 505 * @protected |
| 506 */ |
| 507 _flushProperties() { |
| 508 if (this.__dataPending) { |
| 509 let changedProps = this.__dataPending; |
| 510 this.__dataPending = null; |
| 511 this.__dataCounter++; |
| 512 this._propertiesChanged(this.__data, changedProps, this.__dataOld); |
| 513 this.__dataCounter--; |
| 514 } |
| 515 } |
| 516 |
| 517 /** |
| 518 * Lifecycle callback called the first time properties are being flushed. |
| 519 * Prior to `ready`, all property sets through accessors are queued and |
| 520 * their effects are flushed after this method returns. |
| 521 * |
| 522 * Users may override this function to implement behavior that is |
| 523 * dependent on the element having its properties initialized, e.g. |
| 524 * from defaults (initialized from `constructor`, `_initializeProperties`)
, |
| 525 * `attributeChangedCallback`, or values propagated from host e.g. via |
| 526 * bindings. `super.ready()` must be called to ensure the data system |
| 527 * becomes enabled. |
| 528 * |
| 529 * @public |
| 530 */ |
| 531 ready() { |
| 532 this.__dataReady = true; |
| 533 // Run normal flush |
| 534 this._flushProperties(); |
| 535 } |
| 536 |
| 537 /** |
| 538 * Callback called when any properties with accessors created via |
| 539 * `_createPropertyAccessor` have been set. |
| 540 * |
| 541 * @param {Object} currentProps Bag of all current accessor values |
| 542 * @param {Object} changedProps Bag of properties changed since the last |
| 543 * call to `_propertiesChanged` |
| 544 * @param {Object} oldProps Bag of previous values for each property |
| 545 * in `changedProps` |
| 546 * @protected |
| 547 */ |
| 548 _propertiesChanged(currentProps, changedProps, oldProps) { // eslint-disab
le-line no-unused-vars |
| 549 } |
| 550 |
| 551 /** |
| 552 * Method called to determine whether a property value should be |
| 553 * considered as a change and cause the `_propertiesChanged` callback |
| 554 * to be enqueued. |
| 555 * |
| 556 * The default implementation returns `true` for primitive types if a |
| 557 * strict equality check fails, and returns `true` for all Object/Arrays. |
| 558 * The method always returns false for `NaN`. |
| 559 * |
| 560 * Override this method to e.g. provide stricter checking for |
| 561 * Objects/Arrays when using immutable patterns. |
| 562 * |
| 563 * @param {string} property Property name |
| 564 * @param {*} value New property value |
| 565 * @param {*} old Previous property value |
| 566 * @return {boolean} Whether the property should be considered a change |
| 567 * and enqueue a `_proeprtiesChanged` callback |
| 568 * @protected |
| 569 */ |
| 570 _shouldPropertyChange(property, value, old) { |
| 571 return ( |
| 572 // Strict equality check |
| 573 (old !== value && |
| 574 // This ensures (old==NaN, value==NaN) always returns false |
| 575 (old === old || value === value)) |
| 576 ); |
| 577 } |
| 578 |
| 579 } |
| 580 |
| 581 return PropertyAccessors; |
| 582 |
| 583 }); |
| 584 |
| 585 })(); |
| 586 </script> |
| OLD | NEW |