@embeddable.com/react
Version:
Embeddable React HOCs to embed components.
254 lines (247 loc) • 12.1 kB
JavaScript
import * as React from 'react';
import React__default from 'react';
import { isLoadDataParams, nativeTypes, setValue } from '@embeddable.com/core';
const EmbeddableStateContext = React__default.createContext({});
const useEmbeddableState = (initialState = {}) => {
const ctx = React__default.useContext(EmbeddableStateContext);
React__default.useEffect(() => {
// Only call setter if it exists (when there's a provider)
if (Array.isArray(ctx) && typeof ctx[1] === "function") {
ctx[1](initialState);
}
}, [JSON.stringify(initialState)]);
return ctx;
};
const EmbeddableThemeContext = React__default.createContext({});
const useTheme = () => {
return React__default.useContext(EmbeddableThemeContext);
};
/**
* Cleans up the inputs of an object, removing all sub inputs except for granularity.
* @param obj
*/
function cleanInputsWithGranularity(obj) {
if (!obj || !("inputs" in obj))
return obj;
const { inputs, ...rest } = obj;
if (typeof inputs === "object" && "granularity" in inputs) {
return { ...rest, inputs: { granularity: inputs.granularity } };
}
return rest;
}
/**
* Cleans up the request parameters by removing all sub inputs except for granularity.
* @param requestParams
*/
const cleanupRequestParams = (requestParams) => {
return {
...requestParams,
dimensions: Array.isArray(requestParams.dimensions)
? requestParams.dimensions.map(cleanInputsWithGranularity)
: requestParams.dimensions,
measures: Array.isArray(requestParams.measures)
? requestParams.measures.map(cleanInputsWithGranularity)
: requestParams.measures,
select: Array.isArray(requestParams.select)
? requestParams.select.map((item) => Array.isArray(item)
? item.map(cleanInputsWithGranularity)
: cleanInputsWithGranularity(item))
: requestParams.select,
orderBy: Array.isArray(requestParams.orderBy)
? requestParams.orderBy.map((item) => ({
...item,
property: cleanInputsWithGranularity(item.property),
}))
: requestParams.orderBy,
filters: Array.isArray(requestParams.filters)
? requestParams.filters.map((filter) => ({
...filter,
property: cleanInputsWithGranularity(filter.property),
}))
: requestParams.filters,
};
};
const UPDATE_CLIENT_CONTEXT_EVENT_NAME = "embeddable-event:update-client-context";
const UPDATE_PROPS_EVENT_NAME = "embeddable-event:update-props";
const RELOAD_DATASET_EVENT_NAME = "embeddable-event:reload-dataset";
const LOAD_DATA_RESULT_EVENT_NAME = "embeddable-event:load-data-result";
const UPDATE_THEME_EVENT_NAME = "embeddable-event:update-theme";
const ReducerActionTypes = {
loading: "loading",
error: "error",
data: "data",
};
const reducer = (state, action) => {
switch (action.type) {
case ReducerActionTypes.loading: {
return {
...state,
[action.inputName]: {
data: state[action.inputName]?.data,
isLoading: true,
},
};
}
case ReducerActionTypes.data: {
return {
...state,
[action.inputName]: {
isLoading: false,
data: action.payload.data,
total: action.payload.total,
},
};
}
case ReducerActionTypes.error: {
return {
...state,
[action.inputName]: {
isLoading: false,
error: action.payload.message || action.payload,
},
};
}
}
return state;
};
const createInitialLoadersState = (dataLoaders) => Object.keys(dataLoaders).reduce((loaderState, loaderKey) => ({
...loaderState,
[loaderKey]: { isLoading: true },
}), {});
const deserializeProps = (props, meta) => Object.fromEntries(Object.entries(props).map(([propName, propValue]) => {
const inputPropConfig = meta.inputs?.find((config) => config.name === propName);
const deserialize = typeof inputPropConfig?.type === "string"
? Object.values(nativeTypes).find((type) => type.toString() === inputPropConfig?.type)?.typeConfig?.transform
: inputPropConfig?.type?.typeConfig?.transform;
return [propName, deserialize?.(propValue) ?? propValue];
}));
const getInputValuesFromMeta = (meta) => {
let inputValues = {};
(meta.inputs || []).forEach((input) => {
inputValues = {
...inputValues,
[input.name]: input.defaultValue ?? null,
};
});
return inputValues;
};
function defineComponent(InnerComponent, meta, config = {}) {
function EmbeddableWrapper({ propsUpdateListener, clientContext, embeddableTheme, ...props }) {
const [propsProxy, setPropsProxy] = React.useState(props);
const [clientContextProxy, setClientContextProxy] = React.useState(clientContext);
const embeddableState = React.useState();
const [calculatedOverridableProps, setCalculatedOverridableProps] = React.useState(embeddableTheme ?? {});
const { componentId } = props;
const loadDataResultEventName = `${LOAD_DATA_RESULT_EVENT_NAME}:${componentId}`;
const propsUpdateEventHandler = ({ detail, }) => setPropsProxy(detail);
const clientContextUpdateEventHandler = ({ detail, }) => setClientContextProxy(detail);
const themeUpdateEventHandler = ({ detail, }) => setCalculatedOverridableProps(detail);
React.useEffect(() => {
const notifyDevtoolListener = ({ detail, }) => {
window.__EMBEDDABLE_DEVTOOLS__?.notifyPropsUpdated(componentId, meta, propsProxy, detail);
};
propsUpdateListener.addEventListener(UPDATE_PROPS_EVENT_NAME, notifyDevtoolListener);
return () => propsUpdateListener.removeEventListener(UPDATE_PROPS_EVENT_NAME, notifyDevtoolListener);
}, [propsProxy]);
React.useEffect(() => {
propsUpdateListener.addEventListener(UPDATE_CLIENT_CONTEXT_EVENT_NAME, clientContextUpdateEventHandler);
propsUpdateListener.addEventListener(UPDATE_PROPS_EVENT_NAME, propsUpdateEventHandler);
propsUpdateListener.addEventListener(UPDATE_THEME_EVENT_NAME, themeUpdateEventHandler);
const updatePropsEvent = new CustomEvent("embeddable-event:update-props-listen", {
detail: { componentId },
});
window.dispatchEvent(updatePropsEvent);
const updateClientContextEvent = new CustomEvent("embeddable-event:update-client-context-listen", {
detail: { componentId },
});
window.dispatchEvent(updateClientContextEvent);
return () => {
propsUpdateListener.removeEventListener(UPDATE_CLIENT_CONTEXT_EVENT_NAME, clientContextUpdateEventHandler);
propsUpdateListener.removeEventListener(UPDATE_PROPS_EVENT_NAME, propsUpdateEventHandler);
};
}, []);
const { extendedProps, dataLoaders } = React.useMemo(() => Object.entries({
...getInputValuesFromMeta(meta),
...config?.props?.(deserializeProps(propsProxy, meta), embeddableState, clientContextProxy),
}).reduce((acc, [key, value]) => {
if (isLoadDataParams(value)) {
acc.dataLoaders[key] = value;
}
else {
acc.extendedProps[key] = value;
}
return acc;
}, { extendedProps: {}, dataLoaders: {} }), [propsProxy, config?.props, embeddableState[0], clientContextProxy]);
const [loadersState, dispatch] = React.useReducer(reducer, dataLoaders, createInitialLoadersState);
const handleDataLoaded = (inputName, data, total) => dispatch({
type: ReducerActionTypes.data,
inputName,
payload: { data, total },
});
const handleError = (inputName, error) => dispatch({ type: ReducerActionTypes.error, inputName, payload: error });
const reloadDataset = (inputName, params) => {
dispatch({ type: ReducerActionTypes.loading, inputName });
const error = params.dataLoader(propsUpdateListener, params.requestParams, componentId, inputName);
if (error)
handleError(inputName, error);
};
const handleLoadDataResult = (ev) => {
window.__EMBEDDABLE_DEVTOOLS__?.notifyDataLoaded(componentId, meta, ev.detail);
if (ev.detail.isSuccess) {
handleDataLoaded(ev.detail.propertyName, ev.detail.data, ev.detail.total);
}
else {
handleError(ev.detail.propertyName, ev.detail.error);
}
};
const variableChangedEventHandler = ({ detail, }) => {
const dataLoadersEntries = Object.entries(dataLoaders).filter(([_, params]) => params.requestParams.from.datasetId === detail.datasetId);
window.__EMBEDDABLE_DEVTOOLS__?.notifyVariableUpdated(componentId, meta, detail, Object.fromEntries(dataLoadersEntries));
dataLoadersEntries.forEach(([inputName, params]) => reloadDataset(inputName, params));
};
React.useEffect(() => {
Object.entries(dataLoaders).forEach(([inputName, params]) => reloadDataset(inputName, params));
window.addEventListener(RELOAD_DATASET_EVENT_NAME, variableChangedEventHandler);
propsUpdateListener.addEventListener(loadDataResultEventName, handleLoadDataResult);
return () => {
window.removeEventListener(RELOAD_DATASET_EVENT_NAME, variableChangedEventHandler);
propsUpdateListener.removeEventListener(loadDataResultEventName, handleLoadDataResult);
};
}, [
JSON.stringify(Object.values(dataLoaders).map((loader) => cleanupRequestParams(loader.requestParams))),
]);
const createEvent = (value, eventName) => setValue(propsUpdateListener, value, componentId, eventName);
const events = config?.events;
const eventProps = {};
if (events) {
for (const event in events) {
if (events.hasOwnProperty(event)) {
let eventFunction = events[event];
eventProps[event] = (value) => createEvent(eventFunction(value), event);
}
}
}
return (React.createElement(EmbeddableStateContext.Provider, { value: embeddableState },
React.createElement(EmbeddableThemeContext.Provider, { value: calculatedOverridableProps },
React.createElement(InnerComponent, { ...extendedProps, ...eventProps, ...loadersState }))));
}
EmbeddableWrapper.displayName = `embedded(${InnerComponent.displayName ?? meta.name})`;
return EmbeddableWrapper;
}
const isOperation = (value) => value?.operation && value?.__embeddableVariableMeta;
function defineEditor(InnerComponent, meta, config) {
function EmbeddableWrapper(props) {
const { componentId, initialValue, propsUpdateListener } = props;
const { type: { typeConfig: { transform }, }, } = meta;
const [componentState, setComponentState] = React.useState(initialValue);
const setter = (value) => {
setComponentState(isOperation(value) ? value.value : value);
setValue(propsUpdateListener, value, componentId);
};
return (React.createElement(InnerComponent, { ...config.inputs(transform?.(componentState) || componentState, setter, meta.config), ...(config.mapProps?.(props) ?? {}) }));
}
EmbeddableWrapper.displayName = `embedded(${InnerComponent.displayName ?? meta.name})`;
return EmbeddableWrapper;
}
export { EmbeddableThemeContext, defineComponent, defineEditor, useEmbeddableState, useTheme };
//# sourceMappingURL=index.esm.js.map