UNPKG

@domx/dataelement

Version:

A DataElement base class for handling data state changes

194 lines 7.67 kB
import { EventMap } from "@domx/eventmap"; import { Middleware } from "@domx/middleware"; export { customDataElements, DataElement }; import { RootState } from "./RootState"; ; /** * Used to keep track of how many data elements * are using the same state key. */ const stateRefs = {}; const connectedMiddleware = new Middleware(); const disconnectedMiddleware = new Middleware(); /** * Base class for data elements. */ class DataElement extends EventMap(HTMLElement) { constructor() { super(...arguments); this.__isConnected = false; this.__dataPropertyMetaData = {}; } static applyMiddlware(connectedFn, disconnectedFn) { connectedMiddleware.use(connectedFn); disconnectedMiddleware.use(disconnectedFn); } static clearMiddleware() { connectedMiddleware.clear(); disconnectedMiddleware.clear(); } connectedCallback() { super.connectedCallback && super.connectedCallback(); elementConnected(this); } disconnectedCallback() { super.disconnectedCallback && super.disconnectedCallback(); elementDisconnected(this); } /** * Resets the state and dispatches the changes; * useful when changing the stateId property. * @param states {StateMap} the key is the name of * the state property, and the value is the state to set it to */ refreshState(states) { const dp = this.constructor.dataProperties; Object.keys(states).forEach((stateName) => { const thisEl = this; const changeEvent = dp[stateName].changeEvent; thisEl[stateName] = states[stateName]; this.dispatchEvent(new CustomEvent(changeEvent, { detail: { isSyncUpdate: true } })); }); if (this.__isConnected === true) { elementDisconnected(this); elementConnected(this); } } /** * Dispatches a change event on this DataElement. * @param prop {string} the name of the property to dispatch the change event on; default is "state" * @param change {object} checks for JSON equals before dispatching (props must be in same order). */ dispatchChange(prop = "state", change) { const dp = this.__dataPropertyMetaData; if (change) { if (JSON.stringify(change) === JSON.stringify(this[prop])) { return; } this[prop] = change; } this.dispatchEvent(new CustomEvent(dp[prop].changeEvent)); } } DataElement.eventsStopImmediatePropagation = true; DataElement.__elementName = "data-element"; DataElement.stateIdProperty = "stateId"; DataElement.dataProperties = { state: { changeEvent: "state-changed" } }; /** * Custom DataElement Registry */ const customDataElements = { /** * Defines the custom element with window.customElements.define * and tags the element name for use in RootState. * @param elementName {string} * @param element {CustomElementConstructor} */ define: (elementName, element) => { setProp(element, "__elementName", elementName); window.customElements.define(elementName, element); } }; const elementConnected = (el) => { const ctor = el.constructor; const stateId = getProp(el, ctor.stateIdProperty); const stateIdPath = stateId ? `.${stateId}` : ""; Object.keys(ctor.dataProperties).forEach((propertyName) => { // determine the statePath and window event name const statePath = `${ctor.__elementName}${stateIdPath}.${propertyName}`; const windowEventName = `${statePath}-changed`; // set/update the change event const dp = ctor.dataProperties[propertyName]; const changeEvent = dp.changeEvent || `${propertyName}-changed`; ctor.dataProperties[propertyName] = { changeEvent }; // add to the stateRefs stateRefs[statePath] = stateRefs[statePath] ? stateRefs[statePath] + 1 : 1; // define the local event handler to push changes to RootState // and other elements with the same statePath const localEventHandler = ((event) => { var _a; if (((_a = event.detail) === null || _a === void 0 ? void 0 : _a.isSyncUpdate) !== true) { RootState.set(statePath, getProp(el, propertyName)); triggerGlobalEvent(el, windowEventName); } }); // define the global event handler const windowEventHandler = (event) => { var _a; if (((_a = getProp(event, "detail")) === null || _a === void 0 ? void 0 : _a.sourceElement) !== el) { setProp(el, propertyName, RootState.get(statePath)); triggerSyncEvent(el, changeEvent); } }; // store the meta data on the element el.__dataPropertyMetaData[propertyName] = { statePath, changeEvent, localEventHandler, windowEventHandler, windowEventName }; // set initial state const initialState = RootState.get(statePath); if (initialState === null) { RootState.set(statePath, getProp(el, propertyName)); } else { setProp(el, propertyName, initialState); triggerSyncEvent(el, changeEvent); } // add the event handlers el.addEventListener(changeEvent, localEventHandler); window.addEventListener(windowEventName, windowEventHandler); }); connectedMiddleware.mapThenExecute(getMiddlewareMetaData(el), () => { }, []); el.__isConnected = true; }; const getMiddlewareMetaData = (el) => { const ctor = el.constructor; const metaData = { elementName: ctor.__elementName, element: el, dataProperties: el.__dataPropertyMetaData }; return metaData; }; const triggerSyncEvent = (el, changeEvent) => el.dispatchEvent(new CustomEvent(changeEvent, { detail: { isSyncUpdate: true } })); const triggerGlobalEvent = (el, changeEvent) => window.dispatchEvent(new CustomEvent(changeEvent, { detail: { sourceElement: el } })); /** * Decrements each data properties state path reference. * If 0, then removes the state from RootState. * Also removes the window event handler * @param el {DataElement} */ const elementDisconnected = (el) => { const dpmd = el.__dataPropertyMetaData; Object.keys(dpmd).forEach((propertyName) => { const dp = el.__dataPropertyMetaData[propertyName]; const statePath = dp.statePath; const changeEvent = dp.changeEvent; stateRefs[statePath] = stateRefs[statePath] - 1; stateRefs[statePath] === 0 && RootState.delete(statePath); el.removeEventListener(changeEvent, dp.localEventHandler); window.removeEventListener(dp.windowEventName, dp.windowEventHandler); dp.windowEventHandler = (event) => { }; dp.localEventHandler = (event) => { }; }); disconnectedMiddleware.mapThenExecute(getMiddlewareMetaData(el), () => { }, []); el.__isConnected = false; }; /** Helper for getting dynamic properties */ const getProp = (obj, name) => { //@ts-ignore TS7053 access dynamic property return obj[name]; }; /** Helper for setting dyanmic properties */ const setProp = (obj, name, value) => { //@ts-ignore TS7053 access dynamic property obj[name] = value; }; //# sourceMappingURL=DataElement.js.map