UNPKG

rn-dynamic-ui-render

Version:
407 lines (368 loc) 16.1 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 let result = normalized .split(".") .reduce( (obj, key) => obj ? typeof obj[key] == "number" ? obj[key].toString() : obj[key] : "", root ) || root[path] || path; return result; } 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; } /** * Evaluates a conditional expression and returns the corresponding value. * * This function takes an 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. * * If the expression is not an object or lacks an expression, the original * expression is returned. * * @param {Object} expr - The 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. */ function evaluateConditionalExpression(expr, data) { if (typeof expr !== "object" || !expr.expression) return expr; if (expr.expression === "ternary") { const conditionResult = evaluateCondition(expr.condition, data); return conditionResult ? getPathValue(expr.trueValue, data) || expr.trueValue : getPathValue(expr.falseValue, data) || 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]; if (typeof handlerName === "string" && typeof handlerFn === "function") { // Keep a stable handler reference if (!componentProps[`__handler_${key}`]) { componentProps[`__handler_${key}`] = (...args) => { if (props?.passData === true) { handlerFn(handlers?.item, handlers?.index); } else if (props.passName == true) { handlerFn(...args, props?.name); } else { handlerFn(...args); } }; } componentProps[key] = componentProps[`__handler_${key}`]; } }); } /** * Dynamically resolves values in a props object based on the provided functions. * If a prop value is a string, it checks if the string corresponds to a function * in the functions object. If it does, it assigns the function to the corresponding * key in the componentProps object. If the string does not correspond to a * function, it checks if the prop value has an expression property set to true. * If it does, it evaluates the expression using the getPathValue util and assigns * the result to the corresponding key in the componentProps object. * If the prop value is an object with a uri property, it checks if the uri property * is an object or a string. If it is an object, it evaluates the expression using * the evaluateConditionalExpression util and assigns the result to the uri property * of the componentProps.source object. If it is a string, it evaluates the expression * using the getPathValue util and assigns the result to the uri property of the * componentProps.source object. * If the prop value is a boolean or number, it assigns the value to the corresponding key * in the componentProps object. * If the prop value has an expression property set to "ternary", it evaluates the * expression using the evaluateConditionalExpression util and assigns the result to the * corresponding key in the componentProps object. * If the prop value has a fixedLength property, it formats the value using the * toFixed method with the specified length and assigns the result to the corresponding key * in the componentProps object. * If the prop value has a type property set to "dateTime", it formats the value using * the moment library with the specified format and assigns the result to the corresponding * key in the componentProps object. * @param {Object} props - The properties containing dynamic values. * @param {Object} componentProps - The component props object to be modified. * @param {Object} functions - The functions object containing handler functions. */ 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) { if (typeof props.source?.uri == "object") { componentProps.source = { uri: evaluateConditionalExpression(props.source.uri, functions), }; } else { componentProps.source = { uri: getPathValue(props.source.uri, functions), }; } } else if ( functions[props[key]] != undefined && (typeof functions[props[key]] == "boolean" || typeof functions[props[key]] == "number") ) { // Case: string references a boolean or number componentProps[key] = typeof functions[props[key]] == "number" ? functions[props[key]].toString() : functions[props[key]]; } if (props[key]?.expression == "ternary") { componentProps[key] = evaluateConditionalExpression( componentProps[key], functions ); } if (props?.fixedLength) { const num = Number(componentProps[key]); if (!isNaN(num)) { componentProps[key] = num.toFixed(props.fixedLength); } } if ( props?.type == "dateTime" && (moment(componentProps[key], props.format, true).isValid() || moment(componentProps[key], moment.ISO_8601, true).isValid()) ) { 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, value) => { if ( typeof props[key] === "string" && theme.colors[props[key]] && key.toLowerCase().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) { let visible = true; if (props?.visible != undefined) { if (props?.visibleExpression) { visible = getPathValue(props.visible, functions); } else { visible = functions[props.visible]; } } if (props?.visible?.expression == "ternary") { visible = evaluateConditionalExpression(props.visible, functions); } return visible; }