diff --git a/packages/react-form-renderer/src/field-spy/index.js b/packages/react-form-renderer/src/field-spy/index.js
new file mode 100644
index 000000000..92078f917
--- /dev/null
+++ b/packages/react-form-renderer/src/field-spy/index.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import { useFormState } from 'react-final-form';
+
+const FieldSpy = ({ fields, field, children }) => {
+ const previousValues = React.useRef(Object.fromEntries(fields.map((field) => [field, null])));
+ const [renderCounter, setRenderCounter] = React.useState(0);
+ const memoizedChildren = React.useMemo(() => children(), [renderCounter, field]);
+ const getChangedFields = React.useCallback((prev, next, arr) =>
+ arr.filter((field) => {
+ const nextVal = field.split('.').reduce((o, i) => (o ? o[i] : null), next);
+ if (!prev[field] && !nextVal) {
+ return false;
+ }
+
+ if (prev[field] !== nextVal) {
+ return true;
+ }
+
+ return false;
+ })
+ );
+
+ useFormState({
+ subscription: { values: true },
+ onChange: ({ values }) => {
+ const changedFields = getChangedFields(previousValues.current, values, fields);
+ if (changedFields.length) {
+ setRenderCounter(renderCounter + 1);
+ previousValues.current = { ...values };
+ }
+ },
+ });
+
+ return memoizedChildren;
+};
+
+export default FieldSpy;
diff --git a/packages/react-form-renderer/src/form-renderer/render-form.js b/packages/react-form-renderer/src/form-renderer/render-form.js
index efde3950f..e70972356 100644
--- a/packages/react-form-renderer/src/form-renderer/render-form.js
+++ b/packages/react-form-renderer/src/form-renderer/render-form.js
@@ -1,12 +1,10 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
-import setWith from 'lodash/setWith';
-import cloneDeep from 'lodash/cloneDeep';
-import { Field } from 'react-final-form';
import RendererContext from '../renderer-context';
import Condition from '../condition';
import getConditionTriggers from '../get-condition-triggers';
import prepareComponentProps from '../prepare-component-props';
+import FieldSpy from '../field-spy';
const FormFieldHideWrapper = ({ hideField, children }) => (hideField ?
{children}
: children);
@@ -19,8 +17,8 @@ FormFieldHideWrapper.defaultProps = {
hideField: false,
};
-const ConditionTriggerWrapper = ({ condition, values, children, field }) => (
-
+const ConditionTriggerWrapper = ({ condition, children, field }) => (
+
{children}
);
@@ -29,38 +27,21 @@ ConditionTriggerWrapper.propTypes = {
condition: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
children: PropTypes.node.isRequired,
field: PropTypes.object,
- values: PropTypes.object.isRequired,
};
-const ConditionTriggerDetector = ({ values = {}, triggers = [], children, condition, field }) => {
- const internalTriggers = [...triggers];
- if (internalTriggers.length === 0) {
- return (
-
- {children}
-
- );
- }
-
- const name = internalTriggers.shift();
+const ConditionTriggerDetector = ({ triggers = [], children, condition, field }) => {
return (
-
- {({ input: { value } }) => (
-
+
+ {() => (
+
{children}
-
+
)}
-
+
);
};
ConditionTriggerDetector.propTypes = {
- values: PropTypes.object,
triggers: PropTypes.arrayOf(PropTypes.string),
children: PropTypes.node,
condition: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
diff --git a/packages/react-form-renderer/src/tests/form-renderer/field-spy.test.js b/packages/react-form-renderer/src/tests/form-renderer/field-spy.test.js
new file mode 100644
index 000000000..a09049f3e
--- /dev/null
+++ b/packages/react-form-renderer/src/tests/form-renderer/field-spy.test.js
@@ -0,0 +1,111 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import FormTemplate from '../../../../../__mocks__/mock-form-template';
+import componentTypes from '../../component-types';
+import useFieldApi from '../../use-field-api';
+import FormRenderer from '../../form-renderer';
+
+import FieldSpy from '../../field-spy';
+
+const TextField = (props) => {
+ const { input, placeholder } = useFieldApi(props);
+ return ;
+};
+
+describe('field-spy test', () => {
+ let initialProps;
+
+ beforeEach(() => {
+ initialProps = {
+ FormTemplate,
+ componentMapper: {
+ [componentTypes.TEXT_FIELD]: TextField,
+ },
+ onSubmit: () => {},
+ };
+ });
+
+ it('should re-render child on eligible field change', async () => {
+ const onChangeFn = jest.fn();
+ const schema = {
+ fields: [
+ {
+ component: componentTypes.TEXT_FIELD,
+ name: 'field-1',
+ },
+ {
+ component: componentTypes.TEXT_FIELD,
+ name: 'field-2',
+ },
+ {
+ component: 'listener',
+ name: 'listener',
+ },
+ ],
+ };
+
+ render(
+ (
+
+ {() => {
+ onChangeFn();
+ return <>>;
+ }}
+
+ ),
+ }}
+ />
+ );
+ expect(onChangeFn).toBeCalledTimes(1);
+ await userEvent.type(screen.getByLabelText('field-1'), 's');
+ expect(onChangeFn).toBeCalledTimes(2);
+ });
+
+ it('should not re-render child on ineligible field change', async () => {
+ const onChangeFn = jest.fn();
+ const schema = {
+ fields: [
+ {
+ component: componentTypes.TEXT_FIELD,
+ name: 'field-1',
+ },
+ {
+ component: componentTypes.TEXT_FIELD,
+ name: 'field-2',
+ },
+ {
+ component: 'listener',
+ name: 'listener',
+ },
+ ],
+ };
+
+ render(
+ (
+
+ {() => {
+ onChangeFn();
+ return <>>;
+ }}
+
+ ),
+ }}
+ />
+ );
+ expect(onChangeFn).toBeCalledTimes(1);
+ await userEvent.type(screen.getByLabelText('field-1'), 's');
+ expect(onChangeFn).toBeCalledTimes(1);
+ });
+});