UNPKG

23.4 kBJavaScriptView Raw
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
5 * http://polymer.github.io/LICENSE.txt
6 * The complete set of authors may be found at
7 * http://polymer.github.io/AUTHORS.txt
8 * The complete set of contributors may be found at
9 * http://polymer.github.io/CONTRIBUTORS.txt
10 * Code distributed by Google as part of the polymer project is also
11 * subject to an additional IP rights grant found at
12 * http://polymer.github.io/PATENTS.txt
13 */
14/**
15 * When using Closure Compiler, JSCompiler_renameProperty(property, object) is
16 * replaced at compile time by the munged name for object[property]. We cannot
17 * alias this function, so we have to use a small shim that has the same
18 * behavior when not compiling.
19 */
20const JSCompiler_renameProperty = (prop, _obj) => prop;
21/**
22 * Returns the property descriptor for a property on this prototype by walking
23 * up the prototype chain. Note that we stop just before Object.prototype, which
24 * also avoids issues with Symbol polyfills (core-js, get-own-property-symbols),
25 * which create accessors for the symbols on Object.prototype.
26 */
27const descriptorFromPrototype = (name, proto) => {
28 if (name in proto) {
29 while (proto !== Object.prototype) {
30 if (proto.hasOwnProperty(name)) {
31 return Object.getOwnPropertyDescriptor(proto, name);
32 }
33 proto = Object.getPrototypeOf(proto);
34 }
35 }
36 return undefined;
37};
38export const defaultConverter = {
39 toAttribute(value, type) {
40 switch (type) {
41 case Boolean:
42 return value ? '' : null;
43 case Object:
44 case Array:
45 // if the value is `null` or `undefined` pass this through
46 // to allow removing/no change behavior.
47 return value == null ? value : JSON.stringify(value);
48 }
49 return value;
50 },
51 fromAttribute(value, type) {
52 switch (type) {
53 case Boolean:
54 return value !== null;
55 case Number:
56 return value === null ? null : Number(value);
57 case Object:
58 case Array:
59 return JSON.parse(value);
60 }
61 return value;
62 }
63};
64/**
65 * Change function that returns true if `value` is different from `oldValue`.
66 * This method is used as the default for a property's `hasChanged` function.
67 */
68export const notEqual = (value, old) => {
69 // This ensures (old==NaN, value==NaN) always returns false
70 return old !== value && (old === old || value === value);
71};
72const defaultPropertyDeclaration = {
73 attribute: true,
74 type: String,
75 converter: defaultConverter,
76 reflect: false,
77 hasChanged: notEqual
78};
79const microtaskPromise = Promise.resolve(true);
80const STATE_HAS_UPDATED = 1;
81const STATE_UPDATE_REQUESTED = 1 << 2;
82const STATE_IS_REFLECTING_TO_ATTRIBUTE = 1 << 3;
83const STATE_IS_REFLECTING_TO_PROPERTY = 1 << 4;
84const STATE_HAS_CONNECTED = 1 << 5;
85/**
86 * Base element class which manages element properties and attributes. When
87 * properties change, the `update` method is asynchronously called. This method
88 * should be supplied by subclassers to render updates as desired.
89 */
90export class UpdatingElement extends HTMLElement {
91 constructor() {
92 super();
93 this._updateState = 0;
94 this._instanceProperties = undefined;
95 this._updatePromise = microtaskPromise;
96 this._hasConnectedResolver = undefined;
97 /**
98 * Map with keys for any properties that have changed since the last
99 * update cycle with previous values.
100 */
101 this._changedProperties = new Map();
102 /**
103 * Map with keys of properties that should be reflected when updated.
104 */
105 this._reflectingProperties = undefined;
106 this.initialize();
107 }
108 /**
109 * Returns a list of attributes corresponding to the registered properties.
110 * @nocollapse
111 */
112 static get observedAttributes() {
113 // note: piggy backing on this to ensure we're _finalized.
114 this._finalize();
115 const attributes = [];
116 for (const [p, v] of this._classProperties) {
117 const attr = this._attributeNameForProperty(p, v);
118 if (attr !== undefined) {
119 this._attributeToPropertyMap.set(attr, p);
120 attributes.push(attr);
121 }
122 }
123 return attributes;
124 }
125 /**
126 * Ensures the private `_classProperties` property metadata is created.
127 * In addition to `_finalize` this is also called in `createProperty` to
128 * ensure the `@property` decorator can add property metadata.
129 */
130 /** @nocollapse */
131 static _ensureClassProperties() {
132 // ensure private storage for property declarations.
133 if (!this.hasOwnProperty(JSCompiler_renameProperty('_classProperties', this))) {
134 this._classProperties = new Map();
135 // NOTE: Workaround IE11 not supporting Map constructor argument.
136 const superProperties = Object.getPrototypeOf(this)._classProperties;
137 if (superProperties !== undefined) {
138 superProperties.forEach((v, k) => this._classProperties.set(k, v));
139 }
140 }
141 }
142 /**
143 * Creates a property accessor on the element prototype if one does not exist.
144 * The property setter calls the property's `hasChanged` property option
145 * or uses a strict identity check to determine whether or not to request
146 * an update.
147 * @nocollapse
148 */
149 static createProperty(name, options = defaultPropertyDeclaration) {
150 // Note, since this can be called by the `@property` decorator which
151 // is called before `_finalize`, we ensure storage exists for property
152 // metadata.
153 this._ensureClassProperties();
154 this._classProperties.set(name, options);
155 if (!options.noAccessor) {
156 const superDesc = descriptorFromPrototype(name, this.prototype);
157 let desc;
158 // If there is a super accessor, capture it and "super" to it
159 if (superDesc !== undefined && (superDesc.set && superDesc.get)) {
160 const { set, get } = superDesc;
161 desc = {
162 get() { return get.call(this); },
163 set(value) {
164 const oldValue = this[name];
165 set.call(this, value);
166 this.requestUpdate(name, oldValue);
167 },
168 configurable: true,
169 enumerable: true
170 };
171 }
172 else {
173 const key = typeof name === 'symbol' ? Symbol() : `__${name}`;
174 desc = {
175 get() { return this[key]; },
176 set(value) {
177 const oldValue = this[name];
178 this[key] = value;
179 this.requestUpdate(name, oldValue);
180 },
181 configurable: true,
182 enumerable: true
183 };
184 }
185 Object.defineProperty(this.prototype, name, desc);
186 }
187 }
188 /**
189 * Creates property accessors for registered properties and ensures
190 * any superclasses are also finalized.
191 * @nocollapse
192 */
193 static _finalize() {
194 if (this.hasOwnProperty(JSCompiler_renameProperty('finalized', this)) &&
195 this.finalized) {
196 return;
197 }
198 // finalize any superclasses
199 const superCtor = Object.getPrototypeOf(this);
200 if (typeof superCtor._finalize === 'function') {
201 superCtor._finalize();
202 }
203 this.finalized = true;
204 this._ensureClassProperties();
205 // initialize Map populated in observedAttributes
206 this._attributeToPropertyMap = new Map();
207 // make any properties
208 // Note, only process "own" properties since this element will inherit
209 // any properties defined on the superClass, and finalization ensures
210 // the entire prototype chain is finalized.
211 if (this.hasOwnProperty(JSCompiler_renameProperty('properties', this))) {
212 const props = this.properties;
213 // support symbols in properties (IE11 does not support this)
214 const propKeys = [
215 ...Object.getOwnPropertyNames(props),
216 ...(typeof Object.getOwnPropertySymbols === 'function')
217 ? Object.getOwnPropertySymbols(props)
218 : []
219 ];
220 for (const p of propKeys) {
221 // note, use of `any` is due to TypeSript lack of support for symbol in
222 // index types
223 this.createProperty(p, props[p]);
224 }
225 }
226 }
227 /**
228 * Returns the property name for the given attribute `name`.
229 * @nocollapse
230 */
231 static _attributeNameForProperty(name, options) {
232 const attribute = options.attribute;
233 return attribute === false
234 ? undefined
235 : (typeof attribute === 'string'
236 ? attribute
237 : (typeof name === 'string' ? name.toLowerCase()
238 : undefined));
239 }
240 /**
241 * Returns true if a property should request an update.
242 * Called when a property value is set and uses the `hasChanged`
243 * option for the property if present or a strict identity check.
244 * @nocollapse
245 */
246 static _valueHasChanged(value, old, hasChanged = notEqual) {
247 return hasChanged(value, old);
248 }
249 /**
250 * Returns the property value for the given attribute value.
251 * Called via the `attributeChangedCallback` and uses the property's
252 * `converter` or `converter.fromAttribute` property option.
253 * @nocollapse
254 */
255 static _propertyValueFromAttribute(value, options) {
256 const type = options.type;
257 const converter = options.converter || defaultConverter;
258 const fromAttribute = (typeof converter === 'function' ? converter : converter.fromAttribute);
259 return fromAttribute ? fromAttribute(value, type) : value;
260 }
261 /**
262 * Returns the attribute value for the given property value. If this
263 * returns undefined, the property will *not* be reflected to an attribute.
264 * If this returns null, the attribute will be removed, otherwise the
265 * attribute will be set to the value.
266 * This uses the property's `reflect` and `type.toAttribute` property options.
267 * @nocollapse
268 */
269 static _propertyValueToAttribute(value, options) {
270 if (options.reflect === undefined) {
271 return;
272 }
273 const type = options.type;
274 const converter = options.converter;
275 const toAttribute = converter && converter.toAttribute ||
276 defaultConverter.toAttribute;
277 return toAttribute(value, type);
278 }
279 /**
280 * Performs element initialization. By default captures any pre-set values for
281 * registered properties.
282 */
283 initialize() { this._saveInstanceProperties(); }
284 /**
285 * Fixes any properties set on the instance before upgrade time.
286 * Otherwise these would shadow the accessor and break these properties.
287 * The properties are stored in a Map which is played back after the
288 * constructor runs. Note, on very old versions of Safari (<=9) or Chrome
289 * (<=41), properties created for native platform properties like (`id` or
290 * `name`) may not have default values set in the element constructor. On
291 * these browsers native properties appear on instances and therefore their
292 * default value will overwrite any element default (e.g. if the element sets
293 * this.id = 'id' in the constructor, the 'id' will become '' since this is
294 * the native platform default).
295 */
296 _saveInstanceProperties() {
297 for (const [p] of this.constructor
298 ._classProperties) {
299 if (this.hasOwnProperty(p)) {
300 const value = this[p];
301 delete this[p];
302 if (!this._instanceProperties) {
303 this._instanceProperties = new Map();
304 }
305 this._instanceProperties.set(p, value);
306 }
307 }
308 }
309 /**
310 * Applies previously saved instance properties.
311 */
312 _applyInstanceProperties() {
313 for (const [p, v] of this._instanceProperties) {
314 this[p] = v;
315 }
316 this._instanceProperties = undefined;
317 }
318 connectedCallback() {
319 this._updateState = this._updateState | STATE_HAS_CONNECTED;
320 // Ensure connection triggers an update. Updates cannot complete before
321 // connection and if one is pending connection the `_hasConnectionResolver`
322 // will exist. If so, resolve it to complete the update, otherwise
323 // requestUpdate.
324 if (this._hasConnectedResolver) {
325 this._hasConnectedResolver();
326 this._hasConnectedResolver = undefined;
327 }
328 else {
329 this.requestUpdate();
330 }
331 }
332 /**
333 * Allows for `super.disconnectedCallback()` in extensions while
334 * reserving the possibility of making non-breaking feature additions
335 * when disconnecting at some point in the future.
336 */
337 disconnectedCallback() { }
338 /**
339 * Synchronizes property values when attributes change.
340 */
341 attributeChangedCallback(name, old, value) {
342 if (old !== value) {
343 this._attributeToProperty(name, value);
344 }
345 }
346 _propertyToAttribute(name, value, options = defaultPropertyDeclaration) {
347 const ctor = this.constructor;
348 const attr = ctor._attributeNameForProperty(name, options);
349 if (attr !== undefined) {
350 const attrValue = ctor._propertyValueToAttribute(value, options);
351 // an undefined value does not change the attribute.
352 if (attrValue === undefined) {
353 return;
354 }
355 // Track if the property is being reflected to avoid
356 // setting the property again via `attributeChangedCallback`. Note:
357 // 1. this takes advantage of the fact that the callback is synchronous.
358 // 2. will behave incorrectly if multiple attributes are in the reaction
359 // stack at time of calling. However, since we process attributes
360 // in `update` this should not be possible (or an extreme corner case
361 // that we'd like to discover).
362 // mark state reflecting
363 this._updateState = this._updateState | STATE_IS_REFLECTING_TO_ATTRIBUTE;
364 if (attrValue == null) {
365 this.removeAttribute(attr);
366 }
367 else {
368 this.setAttribute(attr, attrValue);
369 }
370 // mark state not reflecting
371 this._updateState = this._updateState & ~STATE_IS_REFLECTING_TO_ATTRIBUTE;
372 }
373 }
374 _attributeToProperty(name, value) {
375 // Use tracking info to avoid deserializing attribute value if it was
376 // just set from a property setter.
377 if (this._updateState & STATE_IS_REFLECTING_TO_ATTRIBUTE) {
378 return;
379 }
380 const ctor = this.constructor;
381 const propName = ctor._attributeToPropertyMap.get(name);
382 if (propName !== undefined) {
383 const options = ctor._classProperties.get(propName) || defaultPropertyDeclaration;
384 // mark state reflecting
385 this._updateState = this._updateState | STATE_IS_REFLECTING_TO_PROPERTY;
386 this[propName] =
387 ctor._propertyValueFromAttribute(value, options);
388 // mark state not reflecting
389 this._updateState = this._updateState & ~STATE_IS_REFLECTING_TO_PROPERTY;
390 }
391 }
392 /**
393 * Requests an update which is processed asynchronously. This should
394 * be called when an element should update based on some state not triggered
395 * by setting a property. In this case, pass no arguments. It should also be
396 * called when manually implementing a property setter. In this case, pass the
397 * property `name` and `oldValue` to ensure that any configured property
398 * options are honored. Returns the `updateComplete` Promise which is resolved
399 * when the update completes.
400 *
401 * @param name {PropertyKey} (optional) name of requesting property
402 * @param oldValue {any} (optional) old value of requesting property
403 * @returns {Promise} A Promise that is resolved when the update completes.
404 */
405 requestUpdate(name, oldValue) {
406 let shouldRequestUpdate = true;
407 // if we have a property key, perform property update steps.
408 if (name !== undefined && !this._changedProperties.has(name)) {
409 const ctor = this.constructor;
410 const options = ctor._classProperties.get(name) || defaultPropertyDeclaration;
411 if (ctor._valueHasChanged(this[name], oldValue, options.hasChanged)) {
412 // track old value when changing.
413 this._changedProperties.set(name, oldValue);
414 // add to reflecting properties set
415 if (options.reflect === true &&
416 !(this._updateState & STATE_IS_REFLECTING_TO_PROPERTY)) {
417 if (this._reflectingProperties === undefined) {
418 this._reflectingProperties = new Map();
419 }
420 this._reflectingProperties.set(name, options);
421 }
422 // abort the request if the property should not be considered changed.
423 }
424 else {
425 shouldRequestUpdate = false;
426 }
427 }
428 if (!this._hasRequestedUpdate && shouldRequestUpdate) {
429 this._enqueueUpdate();
430 }
431 return this.updateComplete;
432 }
433 /**
434 * Sets up the element to asynchronously update.
435 */
436 async _enqueueUpdate() {
437 // Mark state updating...
438 this._updateState = this._updateState | STATE_UPDATE_REQUESTED;
439 let resolve;
440 const previousUpdatePromise = this._updatePromise;
441 this._updatePromise = new Promise((res) => resolve = res);
442 // Ensure any previous update has resolved before updating.
443 // This `await` also ensures that property changes are batched.
444 await previousUpdatePromise;
445 // Make sure the element has connected before updating.
446 if (!this._hasConnected) {
447 await new Promise((res) => this._hasConnectedResolver = res);
448 }
449 // Allow `performUpdate` to be asynchronous to enable scheduling of updates.
450 const result = this.performUpdate();
451 // Note, this is to avoid delaying an additional microtask unless we need
452 // to.
453 if (result != null &&
454 typeof result.then === 'function') {
455 await result;
456 }
457 resolve(!this._hasRequestedUpdate);
458 }
459 get _hasConnected() {
460 return (this._updateState & STATE_HAS_CONNECTED);
461 }
462 get _hasRequestedUpdate() {
463 return (this._updateState & STATE_UPDATE_REQUESTED);
464 }
465 get hasUpdated() { return (this._updateState & STATE_HAS_UPDATED); }
466 /**
467 * Performs an element update.
468 *
469 * You can override this method to change the timing of updates. For instance,
470 * to schedule updates to occur just before the next frame:
471 *
472 * ```
473 * protected async performUpdate(): Promise<unknown> {
474 * await new Promise((resolve) => requestAnimationFrame(() => resolve()));
475 * super.performUpdate();
476 * }
477 * ```
478 */
479 performUpdate() {
480 // Mixin instance properties once, if they exist.
481 if (this._instanceProperties) {
482 this._applyInstanceProperties();
483 }
484 if (this.shouldUpdate(this._changedProperties)) {
485 const changedProperties = this._changedProperties;
486 this.update(changedProperties);
487 this._markUpdated();
488 if (!(this._updateState & STATE_HAS_UPDATED)) {
489 this._updateState = this._updateState | STATE_HAS_UPDATED;
490 this.firstUpdated(changedProperties);
491 }
492 this.updated(changedProperties);
493 }
494 else {
495 this._markUpdated();
496 }
497 }
498 _markUpdated() {
499 this._changedProperties = new Map();
500 this._updateState = this._updateState & ~STATE_UPDATE_REQUESTED;
501 }
502 /**
503 * Returns a Promise that resolves when the element has completed updating.
504 * The Promise value is a boolean that is `true` if the element completed the
505 * update without triggering another update. The Promise result is `false` if
506 * a property was set inside `updated()`. This getter can be implemented to
507 * await additional state. For example, it is sometimes useful to await a
508 * rendered element before fulfilling this Promise. To do this, first await
509 * `super.updateComplete` then any subsequent state.
510 *
511 * @returns {Promise} The Promise returns a boolean that indicates if the
512 * update resolved without triggering another update.
513 */
514 get updateComplete() { return this._updatePromise; }
515 /**
516 * Controls whether or not `update` should be called when the element requests
517 * an update. By default, this method always returns `true`, but this can be
518 * customized to control when to update.
519 *
520 * * @param _changedProperties Map of changed properties with old values
521 */
522 shouldUpdate(_changedProperties) {
523 return true;
524 }
525 /**
526 * Updates the element. This method reflects property values to attributes.
527 * It can be overridden to render and keep updated element DOM.
528 * Setting properties inside this method will *not* trigger
529 * another update.
530 *
531 * * @param _changedProperties Map of changed properties with old values
532 */
533 update(_changedProperties) {
534 if (this._reflectingProperties !== undefined &&
535 this._reflectingProperties.size > 0) {
536 for (const [k, v] of this._reflectingProperties) {
537 this._propertyToAttribute(k, this[k], v);
538 }
539 this._reflectingProperties = undefined;
540 }
541 }
542 /**
543 * Invoked whenever the element is updated. Implement to perform
544 * post-updating tasks via DOM APIs, for example, focusing an element.
545 *
546 * Setting properties inside this method will trigger the element to update
547 * again after this update cycle completes.
548 *
549 * * @param _changedProperties Map of changed properties with old values
550 */
551 updated(_changedProperties) { }
552 /**
553 * Invoked when the element is first updated. Implement to perform one time
554 * work on the element after update.
555 *
556 * Setting properties inside this method will trigger the element to update
557 * again after this update cycle completes.
558 *
559 * * @param _changedProperties Map of changed properties with old values
560 */
561 firstUpdated(_changedProperties) { }
562}
563/**
564 * Marks class as having finished creating properties.
565 */
566UpdatingElement.finalized = true;
567//# sourceMappingURL=updating-element.js.map
\No newline at end of file