@domx/dataelement
Version:
A DataElement base class for handling data state changes
169 lines • 6.45 kB
JavaScript
import { RootState } from "./RootState";
import { DataElement } from "./DataElement";
import { StateChange } from "@domx/statechange";
export { applyDataElementRdtLogging };
/**
* Redux Dev Tools Middleware
*
* Docs:
* https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Methods.md
*
* Set logChangeEvents to false if using StateChange and
* do not want the duplicate state entry.
* @param options {RdtLoggingOptions}
*/
const applyDataElementRdtLogging = (options = { logChangeEvents: true, exclude: [] }) => {
if (isApplied || hasDevTools() === false) {
return;
}
isApplied = true;
logChangeEvents = options.logChangeEvents ? true : false;
excludeActions = options.exclude ? excludeActions.concat(options.exclude) : excludeActions;
DataElement.applyMiddlware(connectedCallback, disconnectedCallback);
StateChange.applyNextMiddleware(stateChangeNext);
window.addEventListener("rootstate-snapshot", sendSnapshot);
};
let isApplied = false;
let logChangeEvents = true;
let excludeActions = ["domx-"];
;
const sendSnapshot = (event) => {
getDevToolsInstance().send(event.detail.name, event.detail.state);
};
const connectedElements = {};
const connectedCallback = (metaData) => (next) => () => {
const el = metaData.element;
Object.keys(metaData.dataProperties).forEach((propertyName) => {
const dp = metaData.dataProperties[propertyName];
const { statePath, changeEvent } = dp;
// update the connected elements
connectedElements[statePath] = connectedElements[statePath] || [];
connectedElements[statePath].push({
element: el,
changeEvent,
property: propertyName
});
sendStateToDevTools(el, propertyName, statePath, changeEvent);
const rdtListener = ((event) => {
var _a;
!((_a = event.detail) === null || _a === void 0 ? void 0 : _a.isFromDevTools) &&
sendStateToDevTools(el, propertyName, statePath, changeEvent);
});
logChangeEvents && el.addEventListener(changeEvent, rdtListener);
//@ts-ignore TS2339 dynamic property
dp.rdtListener = rdtListener;
});
next();
};
const sendStateToDevTools = (el, propertyName, statePath, changeEvent) => {
// @ts-ignore TS7053 getting indexed property
const nextState = el[propertyName];
const action = `${el.constructor.__elementName}@${changeEvent}`;
if (!excludeActions.find(a => action.indexOf(a) === 0)) {
getDevToolsInstance().send(action, RootState.draft(statePath, nextState));
}
};
const disconnectedCallback = (metaData) => (next) => () => {
const el = metaData.element;
Object.keys(metaData.dataProperties).forEach((propertyName) => {
// update the connected elements
const dp = metaData.dataProperties[propertyName];
const { statePath, changeEvent } = dp;
const elIndex = connectedElements[statePath].findIndex(cde => cde.element === el);
connectedElements[statePath].splice(elIndex, 1);
//@ts-ignore TS2339 dynamic property
el.removeEventListener(changeEvent, dp.rdtLitener);
//@ts-ignore TS2339 dynamic property
delete dp.rdtListener;
});
next();
};
const stateChangeNext = (stateChange) => (next) => (state) => {
const result = next(state);
const meta = stateChange.meta;
const dpmd = meta.el.__dataPropertyMetaData;
const statePath = dpmd[meta.property].statePath;
getDevToolsInstance().send(getFnName(meta), RootState.draft(statePath, result));
return result;
};
const getFnName = ({ className, tapName, nextName }) => `${className}.${tapName ? `${tapName}(${nextName})` : `${nextName}()`}`;
/**
* True if the redux dev tools extension is installed
* @returns {boolean}
*/
const hasDevTools = () => window.__REDUX_DEVTOOLS_EXTENSION__ !== undefined;
/**
* Returns the redux dev tools extension
* @returns {DevToolsExtension}
*/
const getDevTools = () => window.__REDUX_DEVTOOLS_EXTENSION__;
/**
* Pulls the connected dev tools instance from the HTML Element.
* Creates it if it does not exist.
* @param stateChange {StateChange}
* @returns {DevToolsInstance}
*/
let __rdt = null;
const getDevToolsInstance = () => {
__rdt = __rdt || setupDevToolsInstance();
return __rdt;
};
/**
* Creates the dev tools istance and sets up the
* listener for dev tools interactions.
* @returns DevToolsInstance
*/
const setupDevToolsInstance = () => {
const dt = getDevTools().connect();
dt.init(RootState.current);
dt.subscribe(updateFromDevTools(dt));
return dt;
};
const updateFromDevTools = (dt) => (data) => {
if (isInit(data)) {
return;
}
if (canHandleUpdate(data)) {
RootState.init(JSON.parse(data.state));
updateConnectedElements();
}
else {
dt.error(`DataElement RDT logging does not support payload type: ${data.type}:${data.payload.type}`);
}
};
/**
* Loops through connected elements and updates
* their state properties and dispatches the sync change
*/
const updateConnectedElements = () => {
Object.keys(connectedElements).forEach((statePath) => {
const stateAtPath = RootState.get(statePath);
connectedElements[statePath].forEach(({ property, changeEvent, element }) => {
// @ts-ignore TS7053 setting indexed property
element[property] = stateAtPath;
element.dispatchEvent(new CustomEvent(changeEvent, {
detail: { isSyncUpdate: true, isFromDevTools: true }
}));
});
});
};
/**
* Returns true if the listener data is for initializing dev tools state.
* @param data {DevToolsEventData}
* @returns {boolean}
*/
const isInit = (data) => data.type === "START" || data.payload.type === "IMPORT_STATE";
/**
* Returns true if this middleware can handle the listener data.
* @param data {DevToolsEventData}
* @returns {boolean}
*/
const canHandleUpdate = (data) => data.type === "DISPATCH" && (data.payload.type === "JUMP_TO_ACTION" ||
data.payload.type === "JUMP_TO_STATE");
/**
* Exposed for testing
*/
applyDataElementRdtLogging.reset = () => {
isApplied = false;
};
//# sourceMappingURL=applyDataElementRdtLogging.js.map