Skip to content

Commit 4fa3cd5

Browse files
committed
fix(renderer): use custom registerField function
1 parent 678dc37 commit 4fa3cd5

File tree

12 files changed

+196
-16
lines changed

12 files changed

+196
-16
lines changed

__mocks__/with-provider.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react';
33
import { RendererContext } from '@data-driven-forms/react-form-renderer';
44
import Form from '@data-driven-forms/react-form-renderer/form';
55

6-
const RenderWithProvider = ({ value = { formOptions: {} }, children, onSubmit = () => {} }) => {
6+
const RenderWithProvider = ({ value = { formOptions: {internalRegisterField: jest.fn(), internalUnRegisterField: jest.fn()} }, children, onSubmit = () => {} }) => {
77
return (
88
<Form onSubmit={onSubmit}>
99
{() => (

packages/ant-component-mapper/src/tests/slider.test.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import React from 'react';
2-
import { Form as DDFForm } from '@data-driven-forms/react-form-renderer';
2+
import { Form as DDFForm, RendererContext } from '@data-driven-forms/react-form-renderer';
33
import { mount } from 'enzyme';
44
import { Slider as AntSlider, Form as OriginalForm } from 'antd';
55
import Slider from '../slider';
66
import FormGroup from '../form-group';
77

88
const Form = (props) => (
99
<OriginalForm>
10-
<DDFForm onSubmit={jest.fn()} {...props} />
10+
<RendererContext.Provider value={{ formOptions: { internalRegisterField: jest.fn(), internalUnRegisterField: jest.fn() } }}>
11+
<DDFForm onSubmit={jest.fn()} {...props} />
12+
</RendererContext.Provider>
1113
</OriginalForm>
1214
);
1315

packages/react-form-renderer/demo/index.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import React from 'react';
33
import PropTypes from 'prop-types';
44
import ReactDOM from 'react-dom';
5-
import { FormRenderer, useFieldApi, componentTypes } from '../src';
5+
import { FormRenderer, useFieldApi, componentTypes, useFormApi } from '../src';
66
import MuiTextField from '@material-ui/core/TextField';
77
import Grid from '@material-ui/core/Grid';
88

@@ -108,9 +108,18 @@ const TextField = (props) => {
108108
);
109109
};
110110

111-
const fields = [];
111+
const Spy = () => {
112+
const formApi = useFormApi();
113+
console.log(formApi);
114+
return null;
115+
};
116+
117+
const fields = [{
118+
name: 'optionsSpy',
119+
component: 'spy',
120+
}];
112121

113-
for (let index = 0; index < 1000; index++) {
122+
for (let index = 0; index < 10; index++) {
114123
fields.push({
115124
name: `field-${index}`,
116125
label: `Text field ${index}`,
@@ -134,7 +143,8 @@ const App = () => {
134143
<div style={{ padding: 20 }}>
135144
<FormRenderer
136145
componentMapper={{
137-
[componentTypes.TEXT_FIELD]: TextField
146+
[componentTypes.TEXT_FIELD]: TextField,
147+
spy: Spy
138148
}}
139149
onSubmit={console.log}
140150
FormTemplate={MuiFormTemplate}

packages/react-form-renderer/src/field-provider/field-provider.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ComponentType, ReactNode } from 'react';
33
export interface FieldProviderProps<T> {
44
Component?: ComponentType<any>;
55
render?: (props: T) => ReactNode;
6+
skipRegistration?: boolean;
67
}
78

89
declare const FieldProvider: React.ComponentType<FieldProviderProps<object>>;

packages/react-form-renderer/src/form-renderer/form-renderer.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,21 @@ const FormRenderer = ({
2626
...props
2727
}) => {
2828
const [fileInputs, setFileInputs] = useState([]);
29+
const registeredFields = useRef({});
2930
const focusDecorator = useRef(createFocusDecorator());
3031
let schemaError;
3132

33+
const setRegisteredFields = (fn => registeredFields.current = fn({...registeredFields.current}));
34+
const internalRegisterField = (name) => {
35+
setRegisteredFields(prev => prev[name] ? ({...prev, [name]: prev[name] + 1}) : ({...prev, [name]: 1}));
36+
};
37+
38+
const internalUnRegisterField = (name) => {
39+
setRegisteredFields(({[name]: currentField, ...prev}) => currentField && currentField > 1 ? ({[name]: currentField - 1, ...prev}) : prev);
40+
};
41+
42+
const internalGetRegisteredFields = () => Object.entries(registeredFields.current).reduce((acc, [name, value]) => value > 0 ? [...acc, name] : acc, []);
43+
3244
const validatorMapperMerged = { ...defaultValidatorMapper, ...validatorMapper };
3345

3446
try {
@@ -80,8 +92,12 @@ const FormRenderer = ({
8092
reset,
8193
clearOnUnmount,
8294
renderForm,
95+
internalRegisterField,
96+
internalUnRegisterField,
8397
...mutators,
84-
...form
98+
...form,
99+
ffGetRegisteredFields: form.getRegisteredFields,
100+
getRegisteredFields: internalGetRegisteredFields,
85101
}
86102
}}
87103
>

packages/react-form-renderer/src/form-renderer/render-form.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React, { useContext } from 'react';
22
import PropTypes from 'prop-types';
33
import set from 'lodash/set';
4+
import { Field } from 'react-final-form';
45
import RendererContext from '../renderer-context';
56
import Condition from '../condition';
6-
import { Field } from 'react-final-form';
77
import getConditionTriggers from '../get-condition-triggers';
88

99
const FormFieldHideWrapper = ({ hideField, children }) => (hideField ? <div hidden>{children}</div> : children);
@@ -50,7 +50,7 @@ const ConditionTriggerDetector = ({ values = {}, triggers = [], children, condit
5050
condition={condition}
5151
field={field}
5252
>
53-
{children}
53+
{children}
5454
</ConditionTriggerDetector>
5555
)}
5656
</Field>
@@ -161,8 +161,6 @@ SingleField.propTypes = {
161161
resolveProps: PropTypes.func
162162
};
163163

164-
const renderForm = (fields) => {
165-
return fields.map((field) => (Array.isArray(field) ? renderForm(field) : <SingleField key={field.name} {...field} />));
166-
};
164+
const renderForm = (fields) =>fields.map((field) => (Array.isArray(field) ? renderForm(field) : <SingleField key={field.name} {...field} />));
167165

168166
export default renderForm;

packages/react-form-renderer/src/renderer-context/renderer-context.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ export interface FormOptions extends FormApi {
1414
handleSubmit: () => Promise<AnyObject | undefined> | undefined;
1515
clearedValue?: any;
1616
renderForm: (fields: Field[]) => ReactNode[];
17+
internalRegisterField: (name: string) => void;
18+
internalUnregisterField: (name: string) => void;
19+
getRegisteredFields: () => string[];
20+
ffGetRegisteredFields: () => string[];
1721
}
1822

1923
export interface RendererContextValue {

packages/react-form-renderer/src/tests/form-renderer/form-renderer.test.js

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
1-
import React from 'react';
1+
import React, { Fragment } from 'react';
2+
import { act } from 'react-dom/test-utils';
23
import { mount } from 'enzyme';
34
import toJson from 'enzyme-to-json';
45
import FormRenderer from '../../form-renderer';
56
import SchemaErrorComponent from '../../form-renderer/schema-error-component';
67
import componentTypes from '../../component-types';
78
import FormTemplate from '../../../../../__mocks__/mock-form-template';
89
import useFieldApi from '../../use-field-api';
10+
import useFormApi from '../../use-form-api';
11+
12+
const PropsSpy = () => <Fragment />;
13+
const ContextSpy = ({registerSpy, spyFF, ...props}) => {
14+
useFieldApi(props);
15+
const { getRegisteredFields, ffGetRegisteredFields, ...formApi } = useFormApi();
16+
return (
17+
<Fragment>
18+
<button onClick={() => registerSpy(spyFF ? ffGetRegisteredFields() : getRegisteredFields())} id={props.name}></button>
19+
<PropsSpy {...formApi} />
20+
</Fragment>
21+
);
22+
};
23+
24+
const DuplicatedField = ({name, ...props}) => {
25+
useFieldApi({name: name.split('@').pop(), ...props});
26+
return <Fragment />;
27+
};
928

1029
const TextField = (props) => {
1130
const { input } = useFieldApi(props);
@@ -181,4 +200,117 @@ describe('<FormRenderer />', () => {
181200
expect(onSubmit).toHaveBeenCalledWith({ 'initial-convert': [{ value: 5 }, { value: 3 }, { value: 11 }, { value: 999 }] });
182201
});
183202
});
203+
204+
it('should register new field to renderer context', () => {
205+
const registerSpy = jest.fn();
206+
const wrapper = mount(
207+
<FormRenderer
208+
FormTemplate={(props) => <FormTemplate {...props} />}
209+
componentMapper={{
210+
spy: {component: ContextSpy, registerSpy}
211+
}}
212+
schema={{ fields: [{component: 'spy', name: 'should-show'}] }}
213+
onSubmit={jest.fn()}
214+
/>
215+
);
216+
217+
const button = wrapper.find('button#should-show');
218+
act(() => {
219+
button.simulate('click');
220+
});
221+
expect(registerSpy).toHaveBeenCalledWith(['should-show']);
222+
});
223+
224+
it('should un-register field after unmount', () => {
225+
const registerSpy = jest.fn();
226+
const wrapper = mount(
227+
<FormRenderer
228+
FormTemplate={(props) => <FormTemplate {...props} />}
229+
componentMapper={{
230+
...componentMapper,
231+
spy: {component: ContextSpy, registerSpy}
232+
}}
233+
initialValues={{ x: 'a' }}
234+
schema={{ fields: [
235+
{component: 'spy', name: 'trigger'},
236+
{component: 'text-field', name: 'x'},
237+
{component: 'text-field', name: 'field-1', condition: {when: 'x', is: 'a'}}
238+
] }}
239+
onSubmit={jest.fn()}
240+
/>
241+
);
242+
243+
const button = wrapper.find('button#trigger');
244+
act(() => {
245+
button.simulate('click');
246+
});
247+
expect(registerSpy).toHaveBeenCalledWith(['trigger', 'x', 'field-1']);
248+
act(() => {
249+
wrapper.find('input').first().simulate('change', { target: { value: '' } });
250+
});
251+
act(() => {
252+
button.simulate('click');
253+
});
254+
expect(registerSpy).toHaveBeenCalledWith(['trigger', 'x']);
255+
});
256+
257+
it('should not un-register field after unmount with multiple fields coppies', () => {
258+
const registerSpy = jest.fn();
259+
const wrapper = mount(
260+
<FormRenderer
261+
FormTemplate={(props) => <FormTemplate {...props} />}
262+
componentMapper={{
263+
...componentMapper,
264+
spy: {component: ContextSpy, registerSpy},
265+
duplicate: DuplicatedField,
266+
}}
267+
initialValues={{ x: 'a' }}
268+
schema={{ fields: [
269+
{component: 'spy', name: 'trigger'},
270+
{component: 'text-field', name: 'x'},
271+
{component: 'text-field', name: 'field-1', condition: {when: 'x', is: 'a'}},
272+
{component: 'duplicate', name: 'dupe@field-1'}
273+
] }}
274+
onSubmit={jest.fn()}
275+
/>
276+
);
277+
278+
const button = wrapper.find('button#trigger');
279+
act(() => {
280+
button.simulate('click');
281+
});
282+
expect(registerSpy).toHaveBeenCalledWith(['trigger', 'x', 'field-1']);
283+
act(() => {
284+
wrapper.find('input').first().simulate('change', { target: { value: '' } });
285+
});
286+
act(() => {
287+
button.simulate('click');
288+
});
289+
expect(registerSpy).toHaveBeenCalledWith(['trigger', 'x', 'field-1']);
290+
});
291+
292+
it('should skip field registration', () => {
293+
const registerSpy = jest.fn();
294+
const wrapper = mount(
295+
<FormRenderer
296+
FormTemplate={(props) => <FormTemplate {...props} />}
297+
componentMapper={{
298+
...componentMapper,
299+
spy: {component: ContextSpy, registerSpy},
300+
duplicate: DuplicatedField,
301+
}}
302+
initialValues={{ x: 'a' }}
303+
schema={{ fields: [
304+
{component: 'spy', name: 'trigger', skipRegistration: true},
305+
] }}
306+
onSubmit={jest.fn()}
307+
/>
308+
);
309+
310+
const button = wrapper.find('button#trigger');
311+
act(() => {
312+
button.simulate('click');
313+
});
314+
expect(registerSpy).toHaveBeenCalledWith([]);
315+
});
184316
});

packages/react-form-renderer/src/tests/form-renderer/render-form.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ describe('renderForm function', () => {
3232
formOptions: {
3333
renderForm,
3434
getState: () => ({ dirty: true }),
35+
internalRegisterField: jest.fn(),
36+
internalUnRegisterField: jest.fn(),
3537
...props.formOptions
3638
}
3739
}}

packages/react-form-renderer/src/tests/form-renderer/use-field-api.test.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ describe('useFieldApi', () => {
3232
<RendererContext.Provider
3333
value={{
3434
formOptions: {
35-
registerInputFile: registerInputFileSpy
35+
registerInputFile: registerInputFileSpy,
36+
internalRegisterField: jest.fn(),
37+
internalUnRegisterField: jest.fn(),
3638
},
3739
validatorMapper: { required: () => (value) => (!value ? 'required' : undefined) }
3840
}}
@@ -197,7 +199,10 @@ describe('useFieldApi', () => {
197199
<RendererContext.Provider
198200
value={{
199201
validatorMapper: { required: () => (value) => (!value ? 'required' : undefined), url: () => jest.fn() },
200-
formOptions: {}
202+
formOptions: {
203+
internalRegisterField: jest.fn(),
204+
internalUnRegisterField: jest.fn(),
205+
}
201206
}}
202207
>
203208
<TestDummy validate={validate} />

packages/react-form-renderer/src/use-field-api/use-field-api.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface ValidatorType extends Object {
1010
export interface UseFieldApiConfig extends AnyObject {
1111
name: string;
1212
validate?: ValidatorType[];
13+
skipRegistration?: boolean;
1314
useWarnings?: boolean;
1415
}
1516
export interface UseFieldApiComponentConfig extends UseFieldConfig<any> {

packages/react-form-renderer/src/use-field-api/use-field-api.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ const createFieldProps = (name, formOptions) => {
8787
const useFieldApi = ({
8888
name,
8989
resolveProps,
90+
skipRegistration = false,
9091
...props
9192
}) => {
9293
const { validatorMapper, formOptions } = useContext(RendererContext);
@@ -196,6 +197,10 @@ const useFieldApi = ({
196197

197198
useEffect(
198199
() => {
200+
if (!skipRegistration) {
201+
formOptions.internalRegisterField(name);
202+
}
203+
199204
mounted.current = true;
200205
if (field.input.type === 'file') {
201206
formOptions.registerInputFile(field.input.name);
@@ -213,6 +218,10 @@ const useFieldApi = ({
213218
if (field.input.type === 'file') {
214219
formOptions.unRegisterInputFile(field.input.name);
215220
}
221+
222+
if (!skipRegistration) {
223+
formOptions.internalUnRegisterField(name);
224+
}
216225
};
217226
},
218227
// eslint-disable-next-line react-hooks/exhaustive-deps

0 commit comments

Comments
 (0)