UNPKG

ce-decorators

Version:

Custom Element decorators for typescript

347 lines (286 loc) 10.9 kB
/** * Copyright (c) 2018 Mathis Zeiher * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ import { camelToKebapCase, kebapToCamelCase, deserializeValue, serializeValue } from './utils'; import { getClassProperties } from './classproperties'; import { COMPONENT_STATE } from './componentstate'; import { PROPERTY_STATE } from './propertystate'; import { getClassPropertyWatcher } from './classpropertywatcher'; import { getClassPropertyInterceptor } from './classpropertyinterceptors'; import { addComponentToRenderPipeline, removeComponentFromRenderPipeline } from './renderer/pipeRenderer'; import { renderComponent } from './renderer/renderComponent'; /** * interface for an indexable element */ /** * define the render strategy for the control */ export let RENDER_STRATEGY; /** * Base class for all custom elements */ (function (RENDER_STRATEGY) { RENDER_STRATEGY[RENDER_STRATEGY["DEFAULT"] = 0] = "DEFAULT"; RENDER_STRATEGY[RENDER_STRATEGY["LAZY"] = 1] = "LAZY"; RENDER_STRATEGY[RENDER_STRATEGY["PIPELINE_EXPERIMENTAL"] = 2] = "PIPELINE_EXPERIMENTAL"; })(RENDER_STRATEGY || (RENDER_STRATEGY = {})); export class CustomElement extends HTMLElement { /* tslint:disable-next-line */ static _fromAttribute(name, oldValue, newValue, instance) { if (instance._propertyState !== PROPERTY_STATE.REFLECTING) { const propertyName = kebapToCamelCase(name); const classProperty = getClassProperties(this).get(propertyName); oldValue = instance[propertyName]; if (classProperty.converter) { if (classProperty.converter.fromAttribute) { newValue = classProperty.converter.fromAttribute(newValue, classProperty.type); // tslint:disable-line:no-unsafe-any } else { newValue = classProperty.converter(newValue, classProperty.type); // tslint:disable-line:no-unsafe-any } } else { newValue = deserializeValue(newValue, classProperty.type); // tslint:disable-line:no-unsafe-any } if (oldValue !== newValue) { instance._propertyState = PROPERTY_STATE.UPDATE_FROM_ATTRIBUTE; this._fromProperty(propertyName, oldValue, newValue, instance); } } } /* tslint:disable-next-line */ static _fromProperty(propertyKey, oldValue, newValue, instance) { if (oldValue !== newValue) { const classProperty = getClassProperties(this).get(propertyKey); const interceptor = getClassPropertyInterceptor(this, propertyKey); newValue = interceptor.reduce((value, func) => { return func.apply(instance, [oldValue, value]) || value; }, newValue); this._reflectAttributes(classProperty, instance, newValue, propertyKey); instance._propertyState = PROPERTY_STATE.UPDATE_PROPERTY; instance[propertyKey] = newValue; instance._propertyState = PROPERTY_STATE.DIRTY; const watcher = getClassPropertyWatcher(this, propertyKey); watcher.forEach(value => value.apply(instance, [oldValue, newValue])); // tslint:disable-line:no-unsafe-any instance.scheduleRender(); } } /* tslint:disable-next-line */ static _reflectAttributes(classProperty, instance, newValue, propertyKey) { if ((classProperty.reflectAsAttribute || classProperty.reflectAsAttribute === undefined) && instance._componentState !== COMPONENT_STATE.INIT) { if (classProperty.type === Boolean || classProperty.type === String || classProperty.type === Number || classProperty.reflectAsAttribute === true) { if (instance._propertyState !== PROPERTY_STATE.UPDATE_FROM_ATTRIBUTE) { instance._propertyState = PROPERTY_STATE.REFLECTING; if (newValue === false || newValue === null || newValue === undefined) { instance.removeAttribute(camelToKebapCase(propertyKey)); } else { if (classProperty.converter && classProperty.converter.toAttribute) { instance.setAttribute(camelToKebapCase(propertyKey), classProperty.converter.toAttribute(newValue, classProperty.type)); } else { instance.setAttribute(camelToKebapCase(propertyKey), serializeValue(newValue, classProperty.type)); } } } } } } static get observedAttributes() { // filter out states -> type === undefined return Array.from(getClassProperties(this)).filter(value => value[1].type !== undefined).map(value => camelToKebapCase(value[0].toString())); } constructor() { super(); this._renderStrategy = RENDER_STRATEGY.DEFAULT; this._renderCallbackResolver = null; this._componentState = COMPONENT_STATE.INIT; this._propertyState = PROPERTY_STATE.DIRTY; this._renderScheduled = false; this._templateCache = null; this._firstRender = true; this._renderCompletedCallbacks = []; this._constructedCompletedCallbacks = []; this._layoutRAFReference = null; Promise.resolve().then(() => { if (this._componentState === COMPONENT_STATE.INIT) { this._componentState = COMPONENT_STATE.CONSTRUCTED; this._constructedCompletedCallbacks.forEach(value => value()); this._constructedCompletedCallbacks = []; } }); } /** * should return the DOM to be rendered */ /** * is called when the element is attached to the DOM */ componentConnected() {} // tslint:disable-line /** * is called when the element is dettached from the DOM */ componentDisconnected() {} // tslint:disable-line /** * is called just before render() will be exexuted */ componentWillRender() {} // tslint:disable-line /** * is called just after render() will be exexuted */ componentDidRender() {} // tslint:disable-line /** * is called just after the first render() */ componentFirstRender() {} // tslint:disable-line /** * is called after render and broser layouting */ componentDidLayout() {} // tslint:disable-line /** * return element whre the DOM from render will be rendered to */ renderToElement() { if (!this.shadowRoot) { this.attachShadow({ mode: 'open' }); } return this.shadowRoot; } /** * return a Promise which will be resolved after * construction of the element * * @returns Promise<void> promise which will resolve after construction is complete */ async waitForConstruction() { return new Promise(resolve => { this._constructedCompletedCallbacks.push(resolve); }); } /** * return a Promise which will be resolved after a * successfull render * * @returns Promise<void> */ async waitForRender() { return new Promise(resolve => { this._renderCompletedCallbacks.push(resolve); }); } /** * Schedule a new render (the render will only be scheduled) if * the componentstate is CONNECTED and propertystate is DIRTY * * force will force a re-render * * @param force force the re-render */ scheduleRender(force = false) { if (this._componentState === COMPONENT_STATE.CONNECTED && this._propertyState === PROPERTY_STATE.DIRTY && !this._renderScheduled) { this._renderScheduled = true; switch (this._renderStrategy) { case RENDER_STRATEGY.PIPELINE_EXPERIMENTAL: addComponentToRenderPipeline(this); break; case RENDER_STRATEGY.LAZY: if (!force) { new Promise(resolve => { setTimeout(resolve); this._renderCallbackResolver = resolve; }).then(() => { renderComponent.apply(this); this._renderCallbackResolver = null; }); break; } default: Promise.resolve().then(() => { renderComponent.apply(this); }); break; } } else if (force) { if (this._renderScheduled) { if (this._renderCallbackResolver) { Promise.resolve().then(() => { this._renderCallbackResolver(); }); return; } else if (this._renderStrategy === RENDER_STRATEGY.PIPELINE_EXPERIMENTAL) { removeComponentFromRenderPipeline(this); } else { return; // render already scheduled as microtask } } Promise.resolve().then(() => { renderComponent.apply(this); }); } } /** * build-in function please do not override */ connectedCallback() { if (this._componentState === COMPONENT_STATE.INIT || this._componentState === COMPONENT_STATE.CONSTRUCTED) { // on first connected reflect attributes this._componentState = COMPONENT_STATE.CONNECTED; const _originalPropertyState = this._propertyState; this._propertyState = PROPERTY_STATE.REFLECTING; const properties = getClassProperties(this.constructor); properties.forEach((value, key) => { const propValue = this[key.toString()]; if (propValue || propValue === 0) { this.constructor._reflectAttributes(value, this, propValue, key.toString()); } }); this._propertyState = _originalPropertyState; } else { this._componentState = COMPONENT_STATE.CONNECTED; } this.componentConnected(); this.scheduleRender(); } /** * build-in function please do not override */ disconnectedCallback() { this._componentState = COMPONENT_STATE.DISCONNECTED; this.componentDisconnected(); } /** * build-in function please do not override */ attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { this.constructor._fromAttribute(name, oldValue, newValue, this); } } } /** * shorthand for lazy rendered custom element */ export class LazyCustomElement extends CustomElement { constructor() { super(); this._renderStrategy = RENDER_STRATEGY.LAZY; } } /** helper for shimmed browsers */ (function () { // tslint:disable-line if ('ShadyCSS' in window && typeof window.ShadyCSS.ScopingShim.prepareAdoptedCssText === 'undefined') { console.error('Please check your "@webcomponents/webcomponentsjs" polyfill, minimum version 2.2.6 required'); // tslint:disable-line } })(); //# sourceMappingURL=customelement.js.map