diff --git a/packages/form-js-editor/src/features/properties-panel/entries/JSFunctionEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/JSFunctionEntry.js
index ccd86fee3..e1f987060 100644
--- a/packages/form-js-editor/src/features/properties-panel/entries/JSFunctionEntry.js
+++ b/packages/form-js-editor/src/features/properties-panel/entries/JSFunctionEntry.js
@@ -1,4 +1,4 @@
-import { FeelEntry, isFeelEntryEdited, TextAreaEntry, isTextAreaEntryEdited } from '@bpmn-io/properties-panel';
+import { FeelEntry, isFeelEntryEdited, TextAreaEntry, isTextAreaEntryEdited, ToggleSwitchEntry, isToggleSwitchEntryEdited } from '@bpmn-io/properties-panel';
import { get } from 'min-dash';
import { useService, useVariables } from '../hooks';
@@ -12,7 +12,7 @@ export function JSFunctionEntry(props) {
const entries = [
{
id: 'variable-mappings',
- component: VariableMappings,
+ component: FunctionParameters,
editField: editField,
field: field,
isEdited: isFeelEntryEdited,
@@ -25,13 +25,21 @@ export function JSFunctionEntry(props) {
field: field,
isEdited: isTextAreaEntryEdited,
isDefaultVisible: (field) => field.type === 'jsfunc'
+ },
+ {
+ id: 'on-load-only',
+ component: OnLoadOnlyEntry,
+ editField: editField,
+ field: field,
+ isEdited: isToggleSwitchEntryEdited,
+ isDefaultVisible: (field) => field.type === 'jsfunc'
}
];
return entries;
}
-function VariableMappings(props) {
+function FunctionParameters(props) {
const {
editField,
field,
@@ -42,7 +50,7 @@ function VariableMappings(props) {
const variables = useVariables().map(name => ({ name }));
- const path = [ 'variableMappings' ];
+ const path = [ 'functionParameters' ];
const getValue = () => {
return get(field, path, '');
@@ -52,13 +60,23 @@ function VariableMappings(props) {
return editField(field, path, value || '');
};
+ const tooltip =
+ Functions parameters should be described as an object, e.g.:
+
{`{
+ name: user.name,
+ age: user.age
+ }`}
+
;
+
return FeelEntry({
debounce,
feel: 'required',
element: field,
getValue,
id,
- label: 'Variable mappings',
+ label: 'Function parameters',
+ tooltip,
+ description: 'Define the parameters to pass to the javascript context.',
setValue,
variables
});
@@ -87,8 +105,35 @@ function FunctionDefinition(props) {
debounce,
element: field,
getValue,
+ description: 'Access function parameters via `data`, set results with `setValue`, and register cleanup functions with `onCleanup`.',
+ id,
+ label: 'Javascript code',
+ setValue
+ });
+}
+
+function OnLoadOnlyEntry(props) {
+ const {
+ editField,
+ field,
+ id
+ } = props;
+
+ const path = [ 'onLoadOnly' ];
+
+ const getValue = () => {
+ return !!get(field, path, false);
+ };
+
+ const setValue = (value) => {
+ editField(field, path, value);
+ };
+
+ return ToggleSwitchEntry({
+ element: field,
id,
- label: 'Function',
+ label: 'Execute on load only',
+ getValue,
setValue
});
}
diff --git a/packages/form-js-viewer/src/render/components/form-fields/JSFunctionField.js b/packages/form-js-viewer/src/render/components/form-fields/JSFunctionField.js
index 651249273..ec31603b8 100644
--- a/packages/form-js-viewer/src/render/components/form-fields/JSFunctionField.js
+++ b/packages/form-js-viewer/src/render/components/form-fields/JSFunctionField.js
@@ -1,26 +1,27 @@
-import { useCallback, useEffect } from 'preact/hooks';
-import { useExpressionEvaluation, useDeepCompareMemoize } from '../../hooks';
+import { useCallback, useEffect, useState } from 'preact/hooks';
+import { useExpressionEvaluation, useDeepCompareMemoize, usePrevious } from '../../hooks';
import { isObject } from 'min-dash';
const type = 'jsfunc';
export function JSFunctionField(props) {
const { field, onChange } = props;
- const { jsFunction, variableMappings } = field;
+ const { jsFunction, functionParameters, onLoadOnly } = field;
- const data = useExpressionEvaluation(variableMappings);
- const dataMemo = useDeepCompareMemoize(data);
+ const [ loadLatch, setLoadLatch ] = useState(false);
- const evaluateExpression = useCallback(() => {
- try {
+ const paramsEval = useExpressionEvaluation(functionParameters);
+ const params = useDeepCompareMemoize(isObject(paramsEval) ? paramsEval : {});
- // return early if no dependency data
- if (!isObject(dataMemo) || !Object.keys(dataMemo).length) {
- return;
- }
+ const functionMemo = useCallback((params) => {
+
+ const cleanupCallbacks = [];
- const func = new Function('data', 'setResult', jsFunction);
- func(dataMemo, value => onChange({ field, value }));
+ try {
+
+ setLoadLatch(true);
+ const func = new Function('data', 'setValue', 'onCleanup', jsFunction);
+ func(params, value => onChange({ field, value }), callback => cleanupCallbacks.push(callback));
} catch (error) {
@@ -32,11 +33,34 @@ export function JSFunctionField(props) {
console.error('Error evaluating expression:', error);
onChange({ field, value: null });
}
- }, [ jsFunction, dataMemo, field, onChange ]);
+
+ return () => {
+ cleanupCallbacks.forEach(fn => fn());
+ };
+
+ }, [ jsFunction, field, onChange ]);
+
+ const previousFunctionMemo = usePrevious(functionMemo);
+ const previousParams = usePrevious(params);
useEffect(() => {
- evaluateExpression();
- }, [ evaluateExpression ]);
+
+ // reset load latch
+ if (!onLoadOnly && loadLatch) {
+ setLoadLatch(false);
+ }
+
+ const functionChanged = previousFunctionMemo !== functionMemo;
+ const paramsChanged = previousParams !== params;
+ const alreadyLoaded = onLoadOnly && loadLatch;
+
+ const shouldExecute = functionChanged || paramsChanged && !alreadyLoaded;
+
+ if (shouldExecute) {
+ return functionMemo(params);
+ }
+
+ }, [ previousFunctionMemo, functionMemo, previousParams, params, loadLatch, onLoadOnly ]);
return null;
}
@@ -48,6 +72,8 @@ JSFunctionField.config = {
keyed: true,
escapeGridRender: true,
create: (options = {}) => ({
+ jsFunction: 'setValue(data.value)',
+ functionParameters: '={\n value: 42\n}',
...options,
})
};