UNPKG

rn-dynamic-ui-render

Version:
331 lines (291 loc) 12.6 kB
import moment from "moment"; /** * Safely evaluate a dynamic condition coming from a JSON schema. * * @param {boolean|string} condition * @param {Object} handlers Bag of data you want exposed. * ├─ data {Object=} (reactive data / global store) (optional) * ├─ helpers {Object=} (utility fns/constants) (optional) * └─ …anything else you like (row, index, meta, etc.) * * @return {boolean} */ export function evaluateCondition(condition, handlers = {}) { /* 0. Unpack special keys but keep the rest alive --------------------- */ const { data: reactiveData = {}, helpers = {}, ...scopedVars // EVERY other key goes here (row, index, form, etc.) } = handlers; /* 1. Literal booleans short‑circuit --------------------------------- */ if (typeof condition === "boolean") return condition; /* 2. Evaluate string expressions ------------------------------------ */ if (typeof condition === "string") { try { /* 2.a Turn template placeholders into pure JS -------------------- */ const expression = resolvePlaceholders( condition, reactiveData, scopedVars // pass EVERYTHING, not just a single "item" ); /* 2.b Build execution context ------------------------------------ */ const context = { ...reactiveData, // top‑level access like user.name ...scopedVars, // row, index, etc. ...helpers, // dayjs, Math, custom utils… }; /* eslint‑disable no-new-func */ const fn = new Function( "context", `"use strict"; return (function ({ ${Object.keys(context).join(",")} }) { return (${expression}); })(context); ` ); /* eslint‑enable no-new-func */ return Boolean(fn(context)); } catch (err) { console.error("Error evaluating condition:", condition, err); return false; } } /* 3. Anything non‑boolean/non‑string is falsy ----------------------- */ return false; } //-------------------------------------------------------------- // ============== Helper functions ============================= //-------------------------------------------------------------- /** * Replace {{ … }} and $…$ placeholders with live values. * * • {{ full JS expression }} → returns inner JS untouched * • $path.to.value$ → resolved against ANY scoped handler key * • Mixed strings get wrapped in quotes so they stay string literals */ function resolvePlaceholders(str, reactiveData, scopedVars) { if (typeof str !== "string") return str; const trimmed = str.trim(); // 1. Whole‑string JS expression: {{ … }} const full = trimmed.match(/^{{\s*(.*?)\s*}}$/); if (full) return full[1]; // inner code only let out = str; // 2. {{path.to.reactiveData}} –––> from reactiveData out = out.replace(/{{(.*?)}}/g, (_, p) => getPathValue(p.trim(), reactiveData) ); /* 3. $any.deep.path$ –––> from scopedVars -------------------------- */ out = out.replace(/\$(.*?)\$/g, (_, p) => getPathValue(p.trim(), scopedVars)); // 4. Mixed interpolation → treat as string literal in the final eval return `"${out}"`; } /** * Walk an object via dotted / bracket path safely. * Works for reactiveData AND for arbitrary scoped handler keys. */ function getPathValue(path, root) { if (!root || typeof path !== "string" || !path.trim()) return ""; try { const normalized = path .replace(/\['([^']+)'\]/g, ".$1") // foo['bar'] → foo.bar .replace(/\"([^\"]+)\"/g, ".$1") // foo["bar"] → foo.bar .replace(/\[(\d+)\]/g, ".$1") // arr[0] → arr.0 .replace(/^\./, ""); // leading dot return normalized .split(".") .reduce((obj, key) => (obj ? obj[key] : ""), root); } catch (e) { console.error("Error while resolving path:", path, e); return ""; } } /** * Evaluates a style expression and returns the corresponding value. * * This function takes a style expression object and evaluates it based on * the specified expression type. Currently, it supports ternary expressions * where a condition is evaluated, and either a true or false value is returned * based on the result of the condition. * * @param {Object} expr - The style expression object containing the expression type and values. * @param {Object} data - The data used to evaluate the expression condition. * @returns {*} - Returns the evaluated value based on the expression type, or the original expr if not an object or lacks an expression. */ function evaluateStyleExpression(expr, data) { if (typeof expr !== "object" || !expr.expression) return expr; if (expr.expression === "ternary") { const conditionResult = evaluateCondition(expr.condition, data); return conditionResult ? expr.trueValue : expr.falseValue; } return expr; } /** * Attaches event handlers to component properties based on the provided props. * * Iterates over the keys of the `props` object, and if a key maps to a string * that corresponds to a function in the `handlers` object, it attaches that * function to the `componentProps` object. This allows for dynamic binding * of event handlers to component properties. * * @param {Object} props - The properties containing handler names. * @param {Object} handlers - The object containing handler functions. * @param {Object} componentProps - The object to attach handlers to. */ export function handleInteractions(props, handlers, componentProps) { Object.keys(props).forEach((key) => { const handlerName = props[key]; const handlerFn = handlers[handlerName]; // Only process if it's a string pointing to a valid function if (typeof handlerName === "string" && typeof handlerFn === "function") { const alreadyAssigned = componentProps[key]; // Attach a unified handler function componentProps[key] = (...args) => { // 1. If another handler was already set (e.g. by the system), run it first if (typeof alreadyAssigned === "function") { alreadyAssigned(...args); } // 2. If passData is true, call with item/index instead of event args if (props?.passData === true) { handlerFn(handlers?.item, handlers?.index); } else { handlerFn(...args); // Pass the event/payload/etc. } }; } }); } /** * Handles dynamic data binding for a component. * * Iterates over the keys of the `props` object, and if a key maps to a string * that corresponds to a function in the `functions` object, or if the key * has the `expression` property set to true, it will evaluate the expression * and attach the result to the `componentProps` object. * * If a property has the `fixedLength` property set, it will also convert the * value to a fixed length string. * * If a property has the `type` property set to `"dateTime"`, it will also * format the value as a date string using the given `format` string. * * @param {Object} props - The properties containing expressions to evaluate. * @param {Object} componentProps - The object to attach dynamic data to. * @param {Object} functions - The object containing functions to evaluate. */ export function handleDynamicData(props, componentProps, functions) { Object.keys(props).forEach((key) => { if (typeof props[key] === "string" && functions[props[key]]) { componentProps[key] = functions[props[key]]; } else if (typeof props[key] === "string" && props?.expression === true) { componentProps[key] = getPathValue(props[key], functions); } else if (props?.source?.uri && props?.expression === true) { componentProps.source = { uri: getPathValue(props.source.uri, functions), }; } if (props?.fixedLength) { componentProps[key] = parseFloat(componentProps[key])?.toFixed( props.fixedLength ); } if (props?.type == "dateTime") { componentProps[key] = moment(componentProps[key]).format(props.format); } }); } /** * Goes through all the props that end with "style" and applies them to the component. * If the value is a string, it's assumed to be a key in the theme object. * If the value is an object, it's assumed to be a reactive expression and is evaluated * using the evaluateStyleExpression util. * If the styleKey includes "color" and the finalValue is a string, it's assumed to be a * key in the theme.colors object and is replaced with the corresponding value. * If the styleKey includes "fontSize" and the finalValue is a string, it's assumed to be a * key in the theme.font object and is replaced with the corresponding value. * If the styleKey includes "fontWeight" and the finalValue is a string, it's assumed to be a * key in the theme.fontWeight object and is replaced with the corresponding value. * @param {Object} props - The component props * @param {Object} componentProps - The component props object to be modified * @param {Object} theme - The theme object * @param {Object} functions - The functions object */ export function handleStyles(props, componentProps, theme = {}, functions) { Object.entries(props).forEach(([key, value]) => { if (key?.toLowerCase()?.endsWith("style")) { if (Object.prototype.hasOwnProperty.call(theme, value)) { componentProps[key] = theme[value]; } else { if (!componentProps[key]) componentProps[key] = {}; Object.entries(value).forEach(([styleKey, styleValue]) => { let finalValue = styleValue; if (typeof styleValue === "object" && styleValue.expression) { finalValue = evaluateStyleExpression(styleValue, functions); } if ( typeof finalValue === "string" && styleKey.toLowerCase().includes("color") && theme.colors?.[finalValue] ) { finalValue = theme.colors[finalValue]; } if (styleKey.includes("fontSize") && theme.font?.[finalValue]) { finalValue = theme.font[finalValue]; } if ( styleKey.includes("fontWeight") && theme.fontWeight?.[finalValue] ) { finalValue = theme.fontWeight[finalValue]; } componentProps[key] = { ...componentProps[key], [styleKey]: finalValue, }; }); } } }); } /** * Sets the color of a component based on the given properties and theme. * * For example, if a component has a property named `backgroundColor` with a value of * `"primary"`, and the theme has a `colors` object with a `primary` property, then * the component's `backgroundColor` property will be set to the theme's `primary` color. * * @param {Object} props - The properties object containing color settings. * @param {Object} theme - The theme object containing color values. * @param {Object} componentProps - The component props object where the color * values will be set. */ export function handleColor(props, theme, componentProps) { Object.keys(props).forEach((key) => { if ( typeof props[key] === "string" && theme.colors[props[key]] && key.endsWith("color") ) { componentProps[key] = theme.colors[props[key]]; } }); } /** * Determines the visibility of a component based on the given properties. * * @param {Object} props - The properties object containing visibility settings. * @param {Object} functions - An object containing functions for evaluation. * @returns {boolean} - Returns a boolean indicating whether the component should be visible. * If `visible` is defined and `visibleExpression` is present, it evaluates * the expression using `getPathValue`. Otherwise, it returns the result * of a function mapped by `visible`. Defaults to `true` if `visible` is not defined. */ export function handleVisible(props, functions) { if (props?.visible) { if (props?.visibleExpression) { return getPathValue(props.visible, functions); } else { return functions[props.visible]; } } return true; }