From 7846db213e62f80dc44b5913b15a89044c5b3444 Mon Sep 17 00:00:00 2001 From: Jaideep Pannu Date: Sun, 25 May 2025 01:35:02 +0530 Subject: [PATCH 1/4] Initial Commit --- .../src/field-spy/index.js | 37 ++++++++ .../src/form-renderer/render-form.js | 43 ++++----- .../src/tests/form-renderer/field-spy.test.js | 92 +++++++++++++++++++ 3 files changed, 145 insertions(+), 27 deletions(-) create mode 100644 packages/react-form-renderer/src/field-spy/index.js create mode 100644 packages/react-form-renderer/src/tests/form-renderer/field-spy.test.js 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..04c904715 --- /dev/null +++ b/packages/react-form-renderer/src/field-spy/index.js @@ -0,0 +1,37 @@ +import React, { Component } from 'react'; +import FormSpy from '../form-spy'; +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(values), [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; + } )); + const handleChange = React.useCallback(({ values }) => { + const changedFields = getChangedFields(previousValues.current, values, fields); + if (changedFields.length) { + setRenderCounter(renderCounter+1) + previousValues.current = { ...previousValues.current, ...values }; + } + }); + const{values, initialValues}= useFormState({subscription:{values:true, initialValues:true}, onChange: handleChange}) + + + + + return memoizedChildren; +} + +export default FieldSpy; \ No newline at end of file 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..eb1dcab4e 100644 --- a/packages/react-form-renderer/src/form-renderer/render-form.js +++ b/packages/react-form-renderer/src/form-renderer/render-form.js @@ -7,6 +7,7 @@ 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); @@ -19,11 +20,14 @@ FormFieldHideWrapper.defaultProps = { hideField: false, }; -const ConditionTriggerWrapper = ({ condition, values, children, field }) => ( - - {children} - -); +const ConditionTriggerWrapper = ({ condition, values, children, field }) => { +console.log(values) + return ( + + {children} + + ) +}; ConditionTriggerWrapper.propTypes = { condition: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), @@ -32,30 +36,15 @@ ConditionTriggerWrapper.propTypes = { 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} - + + {(values) => ( + + {children} + )} - + ); }; 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..a9068d4e1 --- /dev/null +++ b/packages/react-form-renderer/src/tests/form-renderer/field-spy.test.js @@ -0,0 +1,92 @@ +import React, { useState } from 'react'; +import { act, render, screen, waitFor } 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 { reducer } from '../../condition'; +import FieldSpy from '../../field-spy'; + +const TextField = (props) => { + const { input, placeholder } = useFieldApi(props); + return ; +}; + +describe('condition test', () => { + let initialProps; + + beforeEach(() => { + + initialProps = { + FormTemplate, + componentMapper: { + [componentTypes.TEXT_FIELD]: TextField + }, + onSubmit: (values) => onSubmit(values), + }; + }); + + 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) + }); +} +) \ No newline at end of file From 544bf72713feae3a5a5a0c56483745b6215849b2 Mon Sep 17 00:00:00 2001 From: Jaideep Pannu Date: Sun, 25 May 2025 01:54:55 +0530 Subject: [PATCH 2/4] Lint fix --- .../src/field-spy/index.js | 40 ++++----- .../src/form-renderer/render-form.js | 18 ++-- .../src/tests/form-renderer/field-spy.test.js | 87 +++++++++++-------- 3 files changed, 79 insertions(+), 66 deletions(-) diff --git a/packages/react-form-renderer/src/field-spy/index.js b/packages/react-form-renderer/src/field-spy/index.js index 04c904715..ef42f970a 100644 --- a/packages/react-form-renderer/src/field-spy/index.js +++ b/packages/react-form-renderer/src/field-spy/index.js @@ -1,37 +1,37 @@ -import React, { Component } from 'react'; -import FormSpy from '../form-spy'; +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(values), [renderCounter,field]) +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) + 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; - } )); - const handleChange = React.useCallback(({ values }) => { + }) + ); + + const { values } = useFormState({ + subscription: { values: true }, + onChange: () => { const changedFields = getChangedFields(previousValues.current, values, fields); if (changedFields.length) { - setRenderCounter(renderCounter+1) + setRenderCounter(renderCounter + 1); previousValues.current = { ...previousValues.current, ...values }; } - }); - const{values, initialValues}= useFormState({subscription:{values:true, initialValues:true}, onChange: handleChange}) - - - + }, + }); return memoizedChildren; -} +}; -export default FieldSpy; \ No newline at end of file +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 eb1dcab4e..2a3d624c7 100644 --- a/packages/react-form-renderer/src/form-renderer/render-form.js +++ b/packages/react-form-renderer/src/form-renderer/render-form.js @@ -1,8 +1,5 @@ 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'; @@ -20,28 +17,26 @@ FormFieldHideWrapper.defaultProps = { hideField: false, }; -const ConditionTriggerWrapper = ({ condition, values, children, field }) => { -console.log(values) +const ConditionTriggerWrapper = ({ condition, children, field }) => { return ( - + {children} - ) + ); }; ConditionTriggerWrapper.propTypes = { condition: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), children: PropTypes.node.isRequired, field: PropTypes.object, - values: PropTypes.object.isRequired, }; const ConditionTriggerDetector = ({ triggers = [], children, condition, field }) => { return ( - {(values) => ( - - {children} + {() => ( + + {children} )} @@ -49,7 +44,6 @@ const ConditionTriggerDetector = ({ triggers = [], children, condition, field }) }; 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 index a9068d4e1..3b3acac57 100644 --- 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 @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { act, render, screen, waitFor } from '@testing-library/react'; +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'; @@ -7,7 +7,6 @@ import componentTypes from '../../component-types'; import useFieldApi from '../../use-field-api'; import FormRenderer from '../../form-renderer'; -import { reducer } from '../../condition'; import FieldSpy from '../../field-spy'; const TextField = (props) => { @@ -19,18 +18,17 @@ describe('condition test', () => { let initialProps; beforeEach(() => { - initialProps = { FormTemplate, componentMapper: { - [componentTypes.TEXT_FIELD]: TextField + [componentTypes.TEXT_FIELD]: TextField, }, - onSubmit: (values) => onSubmit(values), + onSubmit: () => {}, }; }); it('should re-render child on eligible field change', async () => { - const onChangeFn = jest.fn() + const onChangeFn = jest.fn(); const schema = { fields: [ { @@ -39,28 +37,39 @@ describe('condition test', () => { }, { component: componentTypes.TEXT_FIELD, - name: 'field-2' + name: 'field-2', }, { - component: "listener", - name:"listener" - } + component: 'listener', + name: 'listener', + }, ], }; - render({() => { -onChangeFn() - return <> - }} - }}/>); -expect(onChangeFn).toBeCalledTimes(1) + render( + ( + + {() => { + onChangeFn(); + return <>; + }} + + ), + }} + /> + ); + expect(onChangeFn).toBeCalledTimes(1); await userEvent.type(screen.getByLabelText('field-1'), 's'); - expect(onChangeFn).toBeCalledTimes(2) + expect(onChangeFn).toBeCalledTimes(2); }); it('should not re-render child on ineligible field change', async () => { - const onChangeFn = jest.fn() + const onChangeFn = jest.fn(); const schema = { fields: [ { @@ -69,24 +78,34 @@ expect(onChangeFn).toBeCalledTimes(1) }, { component: componentTypes.TEXT_FIELD, - name: 'field-2' + name: 'field-2', }, { - component: "listener", - name:"listener" - } + component: 'listener', + name: 'listener', + }, ], }; - render({() => { -onChangeFn() - return <> - }} - }}/>); -expect(onChangeFn).toBeCalledTimes(1) + render( + ( + + {() => { + onChangeFn(); + return <>; + }} + + ), + }} + /> + ); + expect(onChangeFn).toBeCalledTimes(1); await userEvent.type(screen.getByLabelText('field-1'), 's'); - expect(onChangeFn).toBeCalledTimes(1) + expect(onChangeFn).toBeCalledTimes(1); }); -} -) \ No newline at end of file +}); From a68ac89ca40f65a600fab85ba45420abdce4cf2c Mon Sep 17 00:00:00 2001 From: Jaideep Pannu Date: Sun, 25 May 2025 10:10:03 +0530 Subject: [PATCH 3/4] Test Fix --- packages/react-form-renderer/src/field-spy/index.js | 4 ++-- .../src/tests/form-renderer/field-spy.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-form-renderer/src/field-spy/index.js b/packages/react-form-renderer/src/field-spy/index.js index ef42f970a..458c2734f 100644 --- a/packages/react-form-renderer/src/field-spy/index.js +++ b/packages/react-form-renderer/src/field-spy/index.js @@ -20,9 +20,9 @@ const FieldSpy = ({ fields, field, children }) => { }) ); - const { values } = useFormState({ + useFormState({ subscription: { values: true }, - onChange: () => { + onChange: ({ values }) => { const changedFields = getChangedFields(previousValues.current, values, fields); if (changedFields.length) { setRenderCounter(renderCounter + 1); 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 index 3b3acac57..a09049f3e 100644 --- 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 @@ -14,7 +14,7 @@ const TextField = (props) => { return ; }; -describe('condition test', () => { +describe('field-spy test', () => { let initialProps; beforeEach(() => { From 76195bfd8032147221156e8495e2bd5adaaa00cf Mon Sep 17 00:00:00 2001 From: Jaideep Pannu Date: Sun, 25 May 2025 11:35:49 +0530 Subject: [PATCH 4/4] Fix issue when old state have null value --- packages/react-form-renderer/src/field-spy/index.js | 2 +- .../src/form-renderer/render-form.js | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/react-form-renderer/src/field-spy/index.js b/packages/react-form-renderer/src/field-spy/index.js index 458c2734f..92078f917 100644 --- a/packages/react-form-renderer/src/field-spy/index.js +++ b/packages/react-form-renderer/src/field-spy/index.js @@ -26,7 +26,7 @@ const FieldSpy = ({ fields, field, children }) => { const changedFields = getChangedFields(previousValues.current, values, fields); if (changedFields.length) { setRenderCounter(renderCounter + 1); - previousValues.current = { ...previousValues.current, ...values }; + previousValues.current = { ...values }; } }, }); 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 2a3d624c7..e70972356 100644 --- a/packages/react-form-renderer/src/form-renderer/render-form.js +++ b/packages/react-form-renderer/src/form-renderer/render-form.js @@ -17,13 +17,11 @@ FormFieldHideWrapper.defaultProps = { hideField: false, }; -const ConditionTriggerWrapper = ({ condition, children, field }) => { - return ( - - {children} - - ); -}; +const ConditionTriggerWrapper = ({ condition, children, field }) => ( + + {children} + +); ConditionTriggerWrapper.propTypes = { condition: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),