Skip to content

Commit b56795d

Browse files
authored
Merge pull request #651 from rvsia/resolveProps
Introduce resolve props
2 parents 0a168b5 + 8788317 commit b56795d

File tree

8 files changed

+393
-23
lines changed

8 files changed

+393
-23
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@ import { Validator } from "./validators";
22
import { ConditionDefinition } from './condition';
33
import { DataType } from "./data-types";
44
import { AnyObject } from "./common";
5+
import { FieldMetaState, FieldInputProps } from "react-final-form";
6+
import { FormOptions } from "./renderer-context";
57

68
export type FieldAction = [string, ...any[]];
79

810
export interface FieldActions {
911
[key: string]: FieldAction;
1012
}
1113

14+
export interface FieldApi<FieldValue, T extends HTMLElement = HTMLElement> {
15+
meta: FieldMetaState<FieldValue>;
16+
input: FieldInputProps<FieldValue, T>;
17+
}
18+
19+
export type ResolvePropsFunction = (props: AnyObject, fieldApi: FieldApi<any>, formOptions: FormOptions) => AnyObject;
20+
1221
interface Field extends AnyObject {
1322
name: string;
1423
component: string;
@@ -20,6 +29,7 @@ interface Field extends AnyObject {
2029
clearedValue?: any;
2130
clearOnUnmount?: boolean;
2231
actions?: FieldActions;
32+
resolveProps?: ResolvePropsFunction;
2333
}
2434

2535
export default Field;

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

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ const reducer = (state, { type, specialType, validate, arrayValidator, initialVa
5757
}
5858
};
5959

60-
const useFieldApi = ({ name, initializeOnMount, component, render, validate, ...props }) => {
61-
const { actionMapper, validatorMapper, formOptions } = useContext(RendererContext);
60+
const useFieldApi = ({ name, initializeOnMount, component, render, validate, resolveProps, ...props }) => {
61+
const { validatorMapper, formOptions } = useContext(RendererContext);
6262

6363
const [{ type, initialValue, validate: stateValidate, arrayValidator }, dispatch] = useReducer(
6464
reducer,
@@ -154,25 +154,14 @@ const useFieldApi = ({ name, initializeOnMount, component, render, validate, ...
154154
[]
155155
);
156156

157-
/**
158-
* Map actions to props
159-
*/
160-
let overrideProps = {};
161-
if (props.actions) {
162-
Object.keys(props.actions).forEach((prop) => {
163-
const [action, ...args] = props.actions[prop];
164-
overrideProps[prop] = actionMapper[action](...args);
165-
});
166-
}
167-
168157
const { initialValue: _initialValue, clearOnUnmount, dataType, clearedValue, isEqual: _isEqual, ...cleanProps } = props;
169158

170159
/**
171160
* construct component props necessary that would live in field provider
172161
*/
173162
return {
174163
...cleanProps,
175-
...overrideProps,
164+
...(resolveProps ? resolveProps(cleanProps, fieldProps, formOptions) : {}),
176165
...fieldProps,
177166
...(arrayValidator ? { arrayValidator } : {}),
178167
input: {

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

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ FormConditionWrapper.propTypes = {
3535
};
3636

3737
const SingleField = ({ component, condition, hideField, ...rest }) => {
38-
const { componentMapper } = useContext(RendererContext);
38+
const { actionMapper, componentMapper } = useContext(RendererContext);
3939

4040
let componentProps = {
4141
component,
@@ -47,15 +47,52 @@ const SingleField = ({ component, condition, hideField, ...rest }) => {
4747
if (typeof componentBinding === 'object' && Object.prototype.hasOwnProperty.call(componentBinding, 'component')) {
4848
const { component, ...mapperProps } = componentBinding;
4949
Component = component;
50-
componentProps = { ...mapperProps, ...componentProps };
50+
componentProps = {
51+
...mapperProps,
52+
...componentProps,
53+
// merge mapper and field actions
54+
...(mapperProps.actions && rest.actions ? { actions: { ...mapperProps.actions, ...rest.actions } } : {}),
55+
// merge mapper and field resolveProps
56+
...(mapperProps.resolveProps && rest.resolveProps
57+
? {
58+
resolveProps: (...args) => ({
59+
...mapperProps.resolveProps(...args),
60+
...rest.resolveProps(...args)
61+
})
62+
}
63+
: {})
64+
};
5165
} else {
5266
Component = componentBinding;
5367
}
5468

69+
/**
70+
* Map actions to props
71+
*/
72+
let overrideProps = {};
73+
let mergedResolveProps; // new object has to be created because of references
74+
if (componentProps.actions) {
75+
Object.keys(componentProps.actions).forEach((prop) => {
76+
const [action, ...args] = componentProps.actions[prop];
77+
overrideProps[prop] = actionMapper[action](...args);
78+
});
79+
80+
// Merge componentProps resolve props and actions resolve props
81+
if (componentProps.resolveProps && overrideProps.resolveProps) {
82+
mergedResolveProps = (...args) => ({
83+
...componentProps.resolveProps(...args),
84+
...overrideProps.resolveProps(...args)
85+
});
86+
}
87+
88+
// do not pass actions object to components
89+
delete componentProps.actions;
90+
}
91+
5592
return (
5693
<FormConditionWrapper condition={condition}>
5794
<FormFieldHideWrapper hideField={hideField}>
58-
<Component {...componentProps} />
95+
<Component {...componentProps} {...overrideProps} {...(mergedResolveProps && { resolveProps: mergedResolveProps })} />
5996
</FormFieldHideWrapper>
6097
</FormConditionWrapper>
6198
);
@@ -67,7 +104,11 @@ SingleField.propTypes = {
67104
hideField: PropTypes.bool,
68105
dataType: PropTypes.string,
69106
validate: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object])),
70-
initialValue: PropTypes.any
107+
initialValue: PropTypes.any,
108+
actions: PropTypes.shape({
109+
[PropTypes.string]: PropTypes.func
110+
}),
111+
resolveProps: PropTypes.func
71112
};
72113

73114
const renderForm = (fields) => fields.map((field) => (Array.isArray(field) ? renderForm(field) : <SingleField key={field.name} {...field} />));

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

Lines changed: 207 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,6 +1396,7 @@ describe('renderForm function', () => {
13961396
{
13971397
component: componentTypes.TEXT_FIELD,
13981398
name: 'unmounted',
1399+
key: 'unmounted-1',
13991400
initialValue: true,
14001401
initializeOnMount: true,
14011402
condition: {
@@ -1469,10 +1470,7 @@ describe('renderForm function', () => {
14691470
}
14701471
];
14711472

1472-
const CustomComponent = (props) => {
1473-
const { label } = useFieldApi(props);
1474-
return <label>{label}</label>;
1475-
};
1473+
const CustomComponent = ({ label }) => <label>{label}</label>;
14761474

14771475
const wrapper = mount(
14781476
<FormRenderer
@@ -1502,6 +1500,95 @@ describe('renderForm function', () => {
15021500
).toEqual('standard label');
15031501
});
15041502

1503+
it('should use actions from componentMapper', () => {
1504+
const mapperLabel = 'mapper label';
1505+
1506+
const actionMapper = 'loadLabelMapper';
1507+
1508+
const customActionMapper = {
1509+
[actionMapper]: () => mapperLabel
1510+
};
1511+
1512+
const formFields = [
1513+
{
1514+
component: 'custom-component',
1515+
name: 'foo',
1516+
label: 'standard label'
1517+
}
1518+
];
1519+
1520+
const CustomComponent = ({ label }) => <label>{label}</label>;
1521+
1522+
const wrapper = mount(
1523+
<FormRenderer
1524+
FormTemplate={(props) => <FormTemplate {...props} />}
1525+
componentMapper={{
1526+
'custom-component': {
1527+
component: CustomComponent,
1528+
actions: {
1529+
label: [actionMapper]
1530+
}
1531+
}
1532+
}}
1533+
schema={{ fields: formFields }}
1534+
onSubmit={jest.fn()}
1535+
actionMapper={customActionMapper}
1536+
/>
1537+
);
1538+
1539+
expect(wrapper.find('label').text()).toEqual(mapperLabel);
1540+
});
1541+
1542+
it('field actions has a priority over mappers and they are merged', () => {
1543+
const fieldLabel = 'field label';
1544+
const mapperLabel = 'mapper label';
1545+
const mappedId = 'mapper id';
1546+
1547+
const actionField = 'loadLabelField';
1548+
const actionMapper = 'loadLabelMapper';
1549+
const idActionmapper = 'loadId';
1550+
1551+
const customActionMapper = {
1552+
[actionField]: () => fieldLabel,
1553+
[actionMapper]: () => mapperLabel,
1554+
[idActionmapper]: () => mappedId
1555+
};
1556+
1557+
const formFields = [
1558+
{
1559+
component: 'custom-component',
1560+
name: 'foo',
1561+
label: 'standard label',
1562+
actions: {
1563+
label: [actionField]
1564+
}
1565+
}
1566+
];
1567+
1568+
const CustomComponent = ({ label, id }) => <label id={id}>{label}</label>;
1569+
1570+
const wrapper = mount(
1571+
<FormRenderer
1572+
FormTemplate={(props) => <FormTemplate {...props} />}
1573+
componentMapper={{
1574+
'custom-component': {
1575+
component: CustomComponent,
1576+
actions: {
1577+
label: [actionMapper],
1578+
id: [idActionmapper]
1579+
}
1580+
}
1581+
}}
1582+
schema={{ fields: formFields }}
1583+
onSubmit={jest.fn()}
1584+
actionMapper={customActionMapper}
1585+
/>
1586+
);
1587+
1588+
expect(wrapper.find('label').text()).toEqual(fieldLabel);
1589+
expect(wrapper.find('label').props().id).toEqual(mappedId);
1590+
});
1591+
15051592
it('composite mapper component', () => {
15061593
const schema = {
15071594
fields: [
@@ -1531,4 +1618,120 @@ describe('renderForm function', () => {
15311618
expect(className).toEqual('composite-class');
15321619
expect(type).toEqual('number');
15331620
});
1621+
1622+
it('resolve props resolve props', () => {
1623+
const label = 'Some super label';
1624+
const resolveProps = jest.fn().mockImplementation(() => ({ label }));
1625+
1626+
const formFields = [
1627+
{
1628+
component: 'custom-component',
1629+
name: 'foo',
1630+
label: 'standard label',
1631+
resolveProps
1632+
}
1633+
];
1634+
1635+
const CustomComponent = (props) => {
1636+
const { label } = useFieldApi(props);
1637+
return <label>{label}</label>;
1638+
};
1639+
1640+
const wrapper = mount(
1641+
<FormRenderer
1642+
FormTemplate={(props) => <FormTemplate {...props} />}
1643+
componentMapper={{
1644+
'custom-component': CustomComponent
1645+
}}
1646+
schema={{ fields: formFields }}
1647+
onSubmit={jest.fn()}
1648+
/>
1649+
);
1650+
1651+
expect(wrapper.find('label').text()).toEqual(label);
1652+
expect(resolveProps).toHaveBeenCalledWith(
1653+
{ label: 'standard label' },
1654+
expect.objectContaining({ meta: expect.any(Object), input: expect.any(Object) }),
1655+
expect.any(Object)
1656+
);
1657+
});
1658+
1659+
it('resolve props are merged and field has priority ', () => {
1660+
const id = 'someId';
1661+
const mapperLabel = 'mappers label';
1662+
const label = 'Some super label';
1663+
1664+
const formFields = [
1665+
{
1666+
component: 'custom-component',
1667+
name: 'foo',
1668+
label: 'standard label',
1669+
resolveProps: () => ({ label })
1670+
}
1671+
];
1672+
1673+
const CustomComponent = (props) => {
1674+
const { label, id } = useFieldApi(props);
1675+
return <label id={id}>{label}</label>;
1676+
};
1677+
1678+
const wrapper = mount(
1679+
<FormRenderer
1680+
FormTemplate={(props) => <FormTemplate {...props} />}
1681+
componentMapper={{
1682+
'custom-component': {
1683+
component: CustomComponent,
1684+
resolveProps: () => ({
1685+
id,
1686+
label: mapperLabel
1687+
})
1688+
}
1689+
}}
1690+
schema={{ fields: formFields }}
1691+
onSubmit={jest.fn()}
1692+
/>
1693+
);
1694+
1695+
expect(wrapper.find('label').text()).toEqual(label);
1696+
expect(wrapper.find('label').props().id).toEqual(id);
1697+
});
1698+
1699+
it('actions can return resolveProps and it has priority over fields', () => {
1700+
const id = 'someId';
1701+
const label = 'Some super label';
1702+
1703+
const actionMapper = {
1704+
resolveProps: () => () => ({ label })
1705+
};
1706+
1707+
const formFields = [
1708+
{
1709+
component: 'custom-component',
1710+
name: 'foo',
1711+
label: 'standard label',
1712+
resolveProps: () => ({ id, label: 'nonsense' }),
1713+
actions: { resolveProps: ['resolveProps'] }
1714+
}
1715+
];
1716+
1717+
const CustomComponent = (props) => {
1718+
const { label, id } = useFieldApi(props);
1719+
return <label id={id}>{label}</label>;
1720+
};
1721+
1722+
const wrapper = mount(
1723+
<FormRenderer
1724+
FormTemplate={(props) => <FormTemplate {...props} />}
1725+
componentMapper={{
1726+
'custom-component': CustomComponent
1727+
}}
1728+
schema={{ fields: formFields }}
1729+
onSubmit={jest.fn()}
1730+
actionMapper={actionMapper}
1731+
/>
1732+
);
1733+
1734+
expect(wrapper.find('label').text()).toEqual(label);
1735+
expect(wrapper.find('label').props().id).toEqual(id);
1736+
});
15341737
});

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ const schemaNav = [
2828
component: 'constants',
2929
linkText: 'Constants'
3030
},
31+
{
32+
component: 'resolve-props',
33+
linkText: 'Resolve props'
34+
},
3135
{
3236
subHeader: true,
3337
title: 'Validation',

0 commit comments

Comments
 (0)