Skip to content

Commit

Permalink
feat: local expression contexts
Browse files Browse the repository at this point in the history
Related to #796
  • Loading branch information
Skaiir committed Sep 25, 2023
1 parent 50d0b2a commit 1de314c
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { get } from 'min-dash';
import { useContext, useMemo } from 'preact/hooks';
import LocalExpressionContext from '../../render/context/LocalExpressionContext';

import ExpandSvg from '../../render/components/form-fields/icons/Expand.svg';
import CollapseSvg from '../../render/components/form-fields/icons/Collapse.svg';
import XMarkSvg from '../../render/components/form-fields/icons/XMark.svg';
import { wrapExpressionContext } from '../../util';

export default class RepeatRenderManager {

Expand Down Expand Up @@ -61,16 +64,27 @@ export default class RepeatRenderManager {
});
};

// eslint-disable-next-line react-hooks/rules-of-hooks
const parentExpressionContext = useContext(LocalExpressionContext);

return (
<>
{displayValues.map((_, index) => {
{displayValues.map((value, index) => {
const elementProps = {
...restProps,
indexes: { ...(indexes || {}), [ repeaterField.id ]: index }
indexes: { ...(indexes || {}), [ repeaterField.id ]: index },
};

const localExpressionContext = useMemo(() => ({
this: value,
parent: wrapExpressionContext(parentExpressionContext.this, parentExpressionContext),
i: [ ...parentExpressionContext.i , index + 1 ]
}), [ index, value ]);

return <div class="fjs-repeat-row-container">
<RowsRenderer { ...elementProps } />
<LocalExpressionContext.Provider value={ localExpressionContext }>
<RowsRenderer { ...elementProps } />
</LocalExpressionContext.Provider>
<div class="fjs-repeat-row-delete-container">
<button class="fjs-repeat-row-delete" onClick={ () => onDeleteItem(index) }>
<XMarkSvg />
Expand Down
24 changes: 17 additions & 7 deletions packages/form-js-viewer/src/render/components/FormComponent.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import FormField from './FormField';

import PoweredBy from './PoweredBy';
import LocalExpressionContext from '../context/LocalExpressionContext';

import useService from '../hooks/useService';
import { useMemo } from 'preact/hooks';
import { useFilteredFormData, useService } from '../hooks';

const noop = () => {};

Expand Down Expand Up @@ -31,6 +32,14 @@ export default function FormComponent(props) {
onReset();
};

const filteredFormData = useFilteredFormData();

const localExpressionContext = useMemo(() => ({
parent: null,
this: filteredFormData,
i: []
}), [ filteredFormData ]);

return (
<form
class="fjs-form"
Expand All @@ -39,11 +48,12 @@ export default function FormComponent(props) {
aria-label={ ariaLabel }
noValidate
>
<FormField
field={ schema }
onChange={ onChange }
/>

<LocalExpressionContext.Provider value={ localExpressionContext }>
<FormField
field={ schema }
onChange={ onChange }
/>
</LocalExpressionContext.Provider>
<PoweredBy />
</form>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createContext } from 'preact';

const LocalExpressionContext = createContext({
this: null,
parent: null,
i: null
});

export default LocalExpressionContext;
20 changes: 17 additions & 3 deletions packages/form-js-viewer/src/render/hooks/useCondition.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import useService from './useService.js';
import useFilteredFormData from './useFilteredFormData.js';
import { useMemo } from 'preact/hooks';
import { useContext, useMemo } from 'preact/hooks';
import LocalExpressionContext from '../context/LocalExpressionContext.js';
import { wrapExpressionContext } from '../../util';

/**
* Evaluate if condition is met reactively based on the conditionChecker and form data.
Expand All @@ -13,7 +15,19 @@ export default function useCondition(condition) {
const conditionChecker = useService('conditionChecker', false);
const filteredData = useFilteredFormData();

const localContext = useContext(LocalExpressionContext);

const expressionContext = useMemo(() => {

if (localContext) {
return wrapExpressionContext(filteredData, localContext);
}

return filteredData;

}, [ filteredData, localContext ]);

return useMemo(() => {
return conditionChecker ? conditionChecker.check(condition, filteredData) : null;
}, [ conditionChecker, condition, filteredData ]);
return conditionChecker ? conditionChecker.check(condition, expressionContext) : null;
}, [ conditionChecker, condition, expressionContext ]);
}
28 changes: 18 additions & 10 deletions packages/form-js-viewer/src/render/hooks/useExpressionEvaluation.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
import useService from './useService';
import useFilteredFormData from './useFilteredFormData';
import { useMemo } from 'preact/hooks';
import LocalExpressionContext from '../context/LocalExpressionContext';
import { useContext, useMemo } from 'preact/hooks';
import { wrapExpressionContext } from '../../util';

/**
* Evaluate a string reactively based on the expressionLanguage and form data.
* If the string is not an expression, it is returned as is.
* Memoised to minimize re-renders.
*
* @param {string} value
* The function is memoized to minimize re-renders.
*
* @param {string} value - The string to evaluate.
* @returns {any} - Evaluated value or the original value if not an expression.
*/
export default function useExpressionEvaluation(value) {
const formData = useFilteredFormData();

const filteredData = useFilteredFormData();
const localExpressionContext = useContext(LocalExpressionContext);
const expressionLanguage = useService('expressionLanguage');

return useMemo(() => {
const expressionContext = useMemo(() => {
if (localExpressionContext) {
wrapExpressionContext(filteredData, localExpressionContext);
}
return filteredData;
}, [ filteredData, localExpressionContext ]);

return useMemo(() => {
if (expressionLanguage && expressionLanguage.isExpression(value)) {
return expressionLanguage.evaluate(value, formData);
return expressionLanguage.evaluate(value, expressionContext);
}

return value;

}, [ expressionLanguage, formData, value ]);
}, [ expressionLanguage, expressionContext, value ]);
}
16 changes: 15 additions & 1 deletion packages/form-js-viewer/src/render/hooks/useReadonly.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import useService from './useService.js';
import useFilteredFormData from './useFilteredFormData.js';
import LocalExpressionContext from '../context/LocalExpressionContext.js';
import { wrapExpressionContext } from '../../util';
import { useContext, useMemo } from 'preact/hooks';

/**
* Retrieve readonly value of a form field, given it can be an
Expand All @@ -16,6 +19,17 @@ export default function useReadonly(formField, properties = {}) {
const expressionLanguage = useService('expressionLanguage');
const conditionChecker = useService('conditionChecker', false);
const filteredData = useFilteredFormData();
const localContext = useContext(LocalExpressionContext);

const expressionContext = useMemo(() => {

if (localContext) {
return wrapExpressionContext(filteredData, localContext);
}

return filteredData;

}, [ filteredData, localContext ]);

const { readonly } = formField;

Expand All @@ -24,7 +38,7 @@ export default function useReadonly(formField, properties = {}) {
}

if (expressionLanguage && expressionLanguage.isExpression(readonly)) {
return conditionChecker ? conditionChecker.check(readonly, filteredData) : false;
return conditionChecker ? conditionChecker.check(readonly, expressionContext) : false;
}

return readonly || false;
Expand Down
23 changes: 16 additions & 7 deletions packages/form-js-viewer/src/render/hooks/useTemplateEvaluation.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import useService from './useService';
import useFilteredFormData from './useFilteredFormData';
import { useMemo } from 'preact/hooks';
import { useContext, useMemo } from 'preact/hooks';
import LocalExpressionContext from '../context/LocalExpressionContext';
import { wrapExpressionContext } from '../../util';

/**
* Template a string reactively based on form data. If the string is not a template, it is returned as is.
Expand All @@ -13,17 +15,24 @@ import { useMemo } from 'preact/hooks';
* @param {Function} [options.buildDebugString]
*
*/
export default function useTemplateEvaluation(value, options) {
export default function useTemplateEvaluation(value, options = {}) {
const filteredData = useFilteredFormData();
const templating = useService('templating');

return useMemo(() => {
const variableContext = useContext(LocalExpressionContext);

if (templating && templating.isTemplate(value)) {
return templating.evaluate(value, filteredData, options);
const fullData = useMemo(() => {
if (variableContext) {
return wrapExpressionContext(filteredData, variableContext);
}
return filteredData;
}, [ filteredData, variableContext ]);

return useMemo(() => {
if (templating && templating.isTemplate(value)) {
return templating.evaluate(value, fullData, options);
}
return value;

}, [ filteredData, templating, value, options ]);
}, [ fullData, templating, value, options ]);
}

26 changes: 26 additions & 0 deletions packages/form-js-viewer/src/util/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,22 @@ export function clone(data, replacer) {
return JSON.parse(JSON.stringify(data, replacer));
}

/**
* Wrap an expression context with additional local context.
* A version of the local context with underscore-wrapped keys is also injected as fallback.
*
* @param {Object} baseContext - The context to wrap.
* @param {Object} localContext - The local context object.
* @returns {Object} The merged context object.
*/
export function wrapExpressionContext(baseContext, localContext) {
return {
...localContext,
...baseContext,
..._wrapObjectKeysWithUnderscores(localContext)
};
}

/**
* Parse the schema for input variables a form might make use of
*
Expand Down Expand Up @@ -192,4 +208,14 @@ export function runRecursively(formField, fn) {
});

fn(formField);
}

// helpers //////////////////////

function _wrapObjectKeysWithUnderscores(obj) {
const newObj = {};
for (const [ key, value ] of Object.entries(obj)) {
newObj[`_${key}_`] = value;
}
return newObj;
}

0 comments on commit 1de314c

Please sign in to comment.