@domx/dataelement
Version:
A DataElement base class for handling data state changes
194 lines • 7.67 kB
JavaScript
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