Skip to content

Commit 2b6ca00

Browse files
authored
Merge pull request #1218 from zgover/patch-3
feat(react): pass props and child render props
2 parents dfdd6fd + b566909 commit 2b6ca00

File tree

6 files changed

+267
-48
lines changed

6 files changed

+267
-48
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ComponentType, FunctionComponent } from 'react';
1+
import { ComponentType, FunctionComponent, ReactNode } from 'react';
22
import { FormProps } from 'react-final-form';
33
import Schema from '../common-types/schema';
44
import ComponentMapper from '../common-types/component-mapper';
@@ -12,6 +12,7 @@ export interface FormRendererProps extends FormProps {
1212
initialValues?: object;
1313
onCancel?: (values: AnyObject, ...args: any[]) => void;
1414
onReset?: () => void;
15+
onError?: (...args: any[]) => void;
1516
schema: Schema;
1617
clearOnUnmount?: boolean;
1718
clearedValue?: any;
@@ -20,6 +21,8 @@ export interface FormRendererProps extends FormProps {
2021
validatorMapper?: ValidatorMapper;
2122
actionMapper?: ActionMapper;
2223
schemaValidatorMapper?: SchemaValidatorMapper;
24+
FormTemplateProps?: AnyObject;
25+
children?: ReactNode | ((props: FormTemplateRenderProps) => ReactNode)
2326
}
2427

2528
declare const FormRenderer: React.ComponentType<FormRendererProps>;

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

Lines changed: 97 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,119 @@
1-
import React, { useState, useRef } from 'react';
2-
import Form from '../form';
31
import arrayMutators from 'final-form-arrays';
4-
import PropTypes from 'prop-types';
52
import createFocusDecorator from 'final-form-focus';
3+
import PropTypes from 'prop-types';
4+
import React, { useCallback, useMemo, useRef, useState, cloneElement } from 'react';
65

6+
import defaultSchemaValidator from '../default-schema-validator';
7+
import defaultValidatorMapper from '../validator-mapper';
8+
import Form from '../form';
79
import RendererContext from '../renderer-context';
810
import renderForm from './render-form';
9-
import defaultSchemaValidator from '../default-schema-validator';
1011
import SchemaErrorComponent from './schema-error-component';
11-
import defaultValidatorMapper from '../validator-mapper';
12+
13+
const isFunc = (fn) => typeof fn === 'function';
1214

1315
const FormRenderer = ({
16+
actionMapper,
17+
children,
18+
clearedValue,
19+
clearOnUnmount,
1420
componentMapper,
21+
decorators,
1522
FormTemplate,
16-
onSubmit,
23+
FormTemplateProps,
24+
mutators,
1725
onCancel,
26+
onError,
1827
onReset,
19-
clearOnUnmount,
20-
subscription,
21-
clearedValue,
28+
onSubmit,
2229
schema,
23-
validatorMapper,
24-
actionMapper,
2530
schemaValidatorMapper,
31+
subscription,
32+
validatorMapper,
2633
...props
2734
}) => {
2835
const [fileInputs, setFileInputs] = useState([]);
36+
const formFields = useMemo(() => renderForm(schema.fields), [schema]);
2937
const registeredFields = useRef({});
3038
const focusDecorator = useRef(createFocusDecorator());
31-
let schemaError;
39+
const validatorMapperMerged = useMemo(() => {
40+
return { ...defaultValidatorMapper, ...validatorMapper };
41+
}, [validatorMapper]);
42+
const mutatorsMerged = useMemo(() => ({ ...arrayMutators, ...mutators }), [mutators]);
43+
const decoratorsMerged = useMemo(() => [focusDecorator.current, ...(Array.isArray(decorators) ? decorators : [])], [decorators]);
3244

33-
const setRegisteredFields = (fn) => (registeredFields.current = fn({ ...registeredFields.current }));
34-
const internalRegisterField = (name) => {
45+
const handleSubmitCallback = useCallback(
46+
(values, formApi, ...args) => {
47+
return !isFunc(onSubmit) ? undefined : onSubmit(values, { ...formApi, fileInputs }, ...args);
48+
},
49+
[onSubmit, fileInputs]
50+
);
51+
52+
const handleCancelCallback = useCallback(
53+
(getState) => {
54+
return (...args) => onCancel(getState().values, ...args);
55+
},
56+
[onCancel]
57+
);
58+
59+
const handleResetCallback = useCallback(
60+
(reset) =>
61+
(...args) => {
62+
reset();
63+
return !isFunc(onReset) ? void 0 : onReset(...args);
64+
},
65+
[onReset]
66+
);
67+
68+
const handleErrorCallback = useCallback(
69+
(...args) => {
70+
// eslint-disable-next-line no-console
71+
console.error(...args);
72+
return !isFunc(onError) ? void 0 : onError(...args);
73+
},
74+
[onError]
75+
);
76+
77+
const registerInputFile = useCallback((name) => {
78+
setFileInputs((prevFiles) => [...prevFiles, name]);
79+
}, []);
80+
81+
const unRegisterInputFile = useCallback((name) => {
82+
setFileInputs((prevFiles) => [...prevFiles.splice(prevFiles.indexOf(name))]);
83+
}, []);
84+
85+
const setRegisteredFields = useCallback((fn) => {
86+
return (registeredFields.current = fn({ ...registeredFields.current }));
87+
}, []);
88+
89+
const internalRegisterField = useCallback((name) => {
3590
setRegisteredFields((prev) => (prev[name] ? { ...prev, [name]: prev[name] + 1 } : { ...prev, [name]: 1 }));
36-
};
91+
}, []);
3792

38-
const internalUnRegisterField = (name) => {
93+
const internalUnRegisterField = useCallback((name) => {
3994
setRegisteredFields(({ [name]: currentField, ...prev }) => (currentField && currentField > 1 ? { [name]: currentField - 1, ...prev } : prev));
40-
};
41-
42-
const internalGetRegisteredFields = () =>
43-
Object.entries(registeredFields.current).reduce((acc, [name, value]) => (value > 0 ? [...acc, name] : acc), []);
95+
}, []);
4496

45-
const validatorMapperMerged = { ...defaultValidatorMapper, ...validatorMapper };
97+
const internalGetRegisteredFields = useCallback(() => {
98+
const fields = registeredFields.current;
99+
return Object.entries(fields).reduce((acc, [name, value]) => (value > 0 ? [...acc, name] : acc), []);
100+
}, []);
46101

47102
try {
48103
const validatorTypes = Object.keys(validatorMapperMerged);
49104
const actionTypes = actionMapper ? Object.keys(actionMapper) : [];
105+
50106
defaultSchemaValidator(schema, componentMapper, validatorTypes, actionTypes, schemaValidatorMapper);
51107
} catch (error) {
52-
schemaError = error;
53-
// eslint-disable-next-line no-console
54-
console.error(error);
55-
// eslint-disable-next-line no-console
56-
console.log('error: ', error.message);
57-
}
58-
59-
if (schemaError) {
60-
return <SchemaErrorComponent name={schemaError.name} message={schemaError.message} />;
108+
handleErrorCallback('schema-error', error);
109+
return <SchemaErrorComponent name={error.name} message={error.message} />;
61110
}
62111

63-
const registerInputFile = (name) => setFileInputs((prevFiles) => [...prevFiles, name]);
64-
65-
const unRegisterInputFile = (name) => setFileInputs((prevFiles) => [...prevFiles.splice(prevFiles.indexOf(name))]);
66-
67112
return (
68113
<Form
69-
{...props}
70-
onSubmit={(values, formApi, ...args) => onSubmit(values, { ...formApi, fileInputs }, ...args)}
71-
mutators={{ ...arrayMutators }}
72-
decorators={[focusDecorator.current]}
114+
onSubmit={handleSubmitCallback}
115+
mutators={mutatorsMerged}
116+
decorators={decoratorsMerged}
73117
subscription={{ pristine: true, submitting: true, valid: true, ...subscription }}
74118
render={({ handleSubmit, pristine, valid, form: { reset, mutators, getState, submit, ...form } }) => (
75119
<RendererContext.Provider
@@ -82,11 +126,9 @@ const FormRenderer = ({
82126
unRegisterInputFile,
83127
pristine,
84128
onSubmit,
85-
onCancel: onCancel ? (...args) => onCancel(getState().values, ...args) : undefined,
86-
onReset: (...args) => {
87-
onReset && onReset(...args);
88-
reset();
89-
},
129+
onCancel: isFunc(onCancel) ? handleCancelCallback(getState) : undefined,
130+
onReset: handleResetCallback(reset),
131+
onError: handleErrorCallback,
90132
getState,
91133
valid,
92134
clearedValue,
@@ -105,25 +147,32 @@ const FormRenderer = ({
105147
},
106148
}}
107149
>
108-
<FormTemplate formFields={renderForm(schema.fields)} schema={schema} />
150+
{FormTemplate && <FormTemplate formFields={formFields} schema={schema} {...FormTemplateProps} />}
151+
152+
{isFunc(children) && children({ formFields, schema })}
153+
{typeof children === 'object' && cloneElement(children, { formFields, schema })}
109154
</RendererContext.Provider>
110155
)}
156+
{...props}
111157
/>
112158
);
113159
};
114160

115161
FormRenderer.propTypes = {
116-
onSubmit: PropTypes.func.isRequired,
162+
children: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
163+
onSubmit: PropTypes.func,
117164
onCancel: PropTypes.func,
118165
onReset: PropTypes.func,
166+
onError: PropTypes.func,
119167
schema: PropTypes.object.isRequired,
120168
clearOnUnmount: PropTypes.bool,
121169
subscription: PropTypes.shape({ [PropTypes.string]: PropTypes.bool }),
122170
clearedValue: PropTypes.any,
123171
componentMapper: PropTypes.shape({
124172
[PropTypes.string]: PropTypes.oneOfType([PropTypes.node, PropTypes.element, PropTypes.func, PropTypes.elementType]),
125173
}).isRequired,
126-
FormTemplate: PropTypes.elementType.isRequired,
174+
FormTemplate: PropTypes.elementType,
175+
FormTemplateProps: PropTypes.object,
127176
validatorMapper: PropTypes.shape({
128177
[PropTypes.string]: PropTypes.func,
129178
}),
@@ -142,6 +191,8 @@ FormRenderer.propTypes = {
142191
}),
143192
}),
144193
initialValues: PropTypes.object,
194+
decorators: PropTypes.array,
195+
mutators: PropTypes.object,
145196
};
146197

147198
FormRenderer.defaultProps = {

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

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ describe('<FormRenderer />', () => {
104104

105105
expect(screen.getByText('Form could not be rendered, because of invalid form schema.')).toBeInTheDocument();
106106
expect(spy).toHaveBeenCalled();
107-
expect(logSpy).toHaveBeenCalledWith('error: ', expect.any(String));
108107

109108
console = _console; // eslint-disable-line
110109
});
@@ -310,4 +309,80 @@ describe('<FormRenderer />', () => {
310309

311310
expect(registerSpy).toHaveBeenCalledWith([]);
312311
});
312+
313+
describe('children prop', () => {
314+
const ChildrenTemplate = ({ formFields, schema, hideButtons }) => {
315+
const { handleSubmit } = useFormApi();
316+
return (
317+
<form onSubmit={handleSubmit}>
318+
{schema.title}
319+
{formFields}
320+
{!hideButtons && <button type="submit">Child node submit</button>}
321+
</form>
322+
);
323+
};
324+
325+
it('should clone template props to children node', () => {
326+
render(
327+
<FormRenderer componentMapper={componentMapper} schema={schema} onSubmit={jest.fn()}>
328+
<ChildrenTemplate />
329+
</FormRenderer>
330+
);
331+
332+
expect(screen.getByLabelText('component1')).toBeInTheDocument();
333+
expect(screen.getByText('Select field')).toBeInTheDocument();
334+
expect(screen.getByText('Child node submit')).toBeInTheDocument();
335+
});
336+
337+
it('should use children node props', () => {
338+
render(
339+
<FormRenderer componentMapper={componentMapper} schema={schema} onSubmit={jest.fn()}>
340+
<ChildrenTemplate hideButtons />
341+
</FormRenderer>
342+
);
343+
344+
expect(screen.getByLabelText('component1')).toBeInTheDocument();
345+
expect(screen.getByText('Select field')).toBeInTheDocument();
346+
const submitButton = screen.queryByText('Child node submit');
347+
expect(submitButton).toBeNull();
348+
});
349+
350+
it('should submit data from children node', () => {
351+
const submitSpy = jest.fn();
352+
render(
353+
<FormRenderer initialValues={{ foo: 'bar' }} componentMapper={componentMapper} schema={schema} onSubmit={submitSpy}>
354+
<ChildrenTemplate />
355+
</FormRenderer>
356+
);
357+
358+
userEvent.click(screen.getByText('Child node submit'));
359+
360+
expect(submitSpy).toHaveBeenCalledWith({ foo: 'bar' }, expect.any(Object), expect.any(Function));
361+
});
362+
363+
it('should use children render function', () => {
364+
render(
365+
<FormRenderer componentMapper={componentMapper} schema={schema} onSubmit={jest.fn()}>
366+
{(props) => <ChildrenTemplate {...props} />}
367+
</FormRenderer>
368+
);
369+
370+
expect(screen.getByLabelText('component1')).toBeInTheDocument();
371+
expect(screen.getByText('Select field')).toBeInTheDocument();
372+
expect(screen.getByText('Child node submit')).toBeInTheDocument();
373+
});
374+
375+
it('should submit data from children render function', () => {
376+
const submitSpy = jest.fn();
377+
render(
378+
<FormRenderer initialValues={{ foo: 'bar' }} componentMapper={componentMapper} schema={schema} onSubmit={submitSpy}>
379+
{(props) => <ChildrenTemplate {...props} />}
380+
</FormRenderer>
381+
);
382+
383+
userEvent.click(screen.getByText('Child node submit'));
384+
385+
expect(submitSpy).toHaveBeenCalledWith({ foo: 'bar' }, expect.any(Object), expect.any(Function));
386+
});
387+
});
313388
});

packages/react-renderer-demo/src/components/navigation/schemas/renderer.schema.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ const schemaRenderer = [
1212
component: 'form-template',
1313
linkText: 'Form template',
1414
},
15+
{
16+
component: 'children',
17+
linkText: 'Children',
18+
},
1519
{
1620
subHeader: true,
1721
title: 'Form components',

0 commit comments

Comments
 (0)