Skip to content

feat(renderer): enable condition set as a function #1331

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/common/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ const devConfig = {
filename: '[name].[hash].js'
},
devtool: 'eval-source-map',
resolve: {
fallback: {
process: 'process/browser.js',
},
},
plugins: [
htmlPlugin,
new webpack.ProvidePlugin({
process: 'process/browser'
process: 'process/browser.js'
})
],
module: {
Expand Down
52 changes: 14 additions & 38 deletions packages/react-form-renderer/demo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,59 +7,35 @@ import FormTemplate from './form-template';
import mapper from './form-fields-mapper';

const schema = {
title: 'Sequence condition',
title: 'Set action',
fields: [
{
component: 'text-field',
name: 'field-1',
label: 'first name',
name: 'useDefaultNickName',
label: 'Do you want to use default nickname?',
description: 'set: {} is used to reset the setter',
},
{
component: 'text-field',
name: 'field-2',
label: 'last name',
},
{
component: 'text-field',
name: 'field-3',
label: 'occupation',
name: 'nickname',
label: 'Nickname',
condition: {
sequence: [
{
and: [
{ when: 'field-1', is: 'james' },
{ when: 'field-2', is: 'bond' },
],
then: { set: { 'field-3': 'SPY' } },
else: { visible: true },
},
{
and: [
{ when: 'field-1', is: 'steve' },
{ when: 'field-2', is: 'jobs' },
],
then: { set: { 'field-3': 'CEO' } },
else: { visible: true },
when: 'useDefaultNickName',
is: 'a',
then: {
set: (formState, getFieldState) => {
return { nickname: formState.values.useDefaultNickName };
},
],
},
else: { visible: true, set: {} },
},
},
],
};
const App = () => {
return (
<div style={{ padding: 20 }}>
<FormRenderer
initialValues={{
'field-1': 'steve',
'field-2': 'jobs',
'field-3': 'RETIRED',
}}
componentMapper={mapper}
onSubmit={console.log}
FormTemplate={FormTemplate}
schema={schema}
/>
<FormRenderer componentMapper={mapper} onSubmit={console.log} FormTemplate={FormTemplate} schema={schema} />
</div>
);
};
Expand Down
4 changes: 3 additions & 1 deletion packages/react-form-renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
"forms",
"javascript"
],
"devDependencies": {},
"devDependencies": {
"process": "^0.11.10"
},
"dependencies": {
"final-form": "^4.20.4",
"final-form-arrays": "^3.0.2",
Expand Down
5 changes: 3 additions & 2 deletions packages/react-form-renderer/src/condition/condition.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Field from "../common-types/field";

import type { FormState, FormApi } from "final-form";

export interface ActionResolution {
visible?: boolean;
set?: object; // TO DO specify this
set?: object | ((formState:FormState<Record<string, any>>, getFieldState:FormApi["getFieldState"]) => object) ; // TO DO specify this
}

export type InnerWhenFunction = (currentField: string) => string;
Expand Down
36 changes: 30 additions & 6 deletions packages/react-form-renderer/src/condition/condition.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import React, { useEffect, useReducer } from 'react';
import React, { useCallback, useEffect, useReducer } from 'react';
import PropTypes from 'prop-types';
import isEqual from 'lodash/isEqual';

import useFormApi from '../use-form-api';
import parseCondition from '../parse-condition';

const setterValueCheck = (setterValue) => {
if (setterValue === null || Array.isArray(setterValue)) {
console.error('Received invalid setterValue. Expected object, received: ', setterValue);

return false;
}

return typeof setterValue === 'object';
};

export const reducer = (state, { type, sets }) => {
switch (type) {
case 'formResetted':
Expand Down Expand Up @@ -42,6 +52,12 @@ const Condition = React.memo(
}
}, [dirty]);

const setValue = useCallback((setter) => {
Object.entries(setter).forEach(([name, value]) => {
formOptions.change(name, value);
});
}, []);

useEffect(() => {
if (setters && setters.length > 0 && (state.initial || !isEqual(setters, state.sets))) {
setters.forEach((setter, index) => {
Expand All @@ -60,9 +76,17 @@ const Condition = React.memo(
*/
if (!meta || isFormModified || typeof meta.initial === 'undefined') {
formOptions.batch(() => {
Object.entries(setter).forEach(([name, value]) => {
formOptions.change(name, value);
});
if (typeof setter !== 'function') {
setValue(setter);
} else {
const setterValue = setter(formOptions.getState(), formOptions.getFieldState);

if (setterValueCheck(setterValue)) {
setValue(setterValue);
} else {
console.error('Received invalid setterValue. Expected object, received: ', setterValue);
}
}
});
}
});
Expand Down Expand Up @@ -101,11 +125,11 @@ const conditionProps = {
notMatch: PropTypes.any,
then: PropTypes.shape({
visible: PropTypes.bool,
set: PropTypes.object,
set: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
}),
else: PropTypes.shape({
visible: PropTypes.bool,
set: PropTypes.object,
set: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
}),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const checkConditionalAction = (type, action, fieldName) => {
`);
}

if (action.hasOwnProperty('set') && (typeof action.set !== 'object' || Array.isArray(action.set))) {
if (action.hasOwnProperty('set') && ((typeof action.set !== 'object' && typeof action.set !== 'function') || Array.isArray(action.set))) {
throw new DefaultSchemaError(`
Error occured in field definition with "name" property: "${fieldName}".
'set' property in action "${type}" has to be a object! Received: ${typeof action.visible}, isArray: ${Array.isArray(action.set)}.
Expand Down
144 changes: 144 additions & 0 deletions packages/react-form-renderer/src/tests/form-renderer/condition.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,150 @@ describe('condition test', () => {
await waitFor(() => expect(screen.getByLabelText('field2')).toHaveValue('set with then'));
});

it('should change fields value by set funciton', async () => {
const schema = {
fields: [
{
component: 'text-field',
name: 'field1',
label: 'field1',
},
{
component: 'text-field',
name: 'field2',
label: 'field2',
condition: {
when: 'field1',
is: 'foo',
then: {
set: (formState) => {
return { field2: formState.values.field1 };
},
},
else: { visible: true, set: {} },
},
},
],
};

render(<FormRenderer {...initialProps} schema={schema} />);
expect(screen.getByLabelText('field2')).toHaveValue('');

await userEvent.type(screen.getByLabelText('field1'), 'foo');
await waitFor(() => expect(screen.getByLabelText('field2')).toHaveValue('foo'));
});

it('check the set function received valid arguments', async () => {
const setSpy = jest.fn();

setSpy.mockImplementation(() => ({}));

const schema = {
fields: [
{
component: 'text-field',
name: 'field1',
label: 'field1',
},
{
component: 'text-field',
name: 'field2',
label: 'field2',
condition: {
when: 'field1',
is: 'foo',
then: {
set: setSpy,
},
else: { visible: true, set: {} },
},
},
],
};

render(<FormRenderer {...initialProps} schema={schema} />);

await userEvent.type(screen.getByLabelText('field1'), 'foo');

await waitFor(() => expect(setSpy).toHaveBeenCalledTimes(1));

const expected = { active: 'field1', dirty: true };

await waitFor(() => expect(setSpy).toHaveBeenCalledWith(expect.objectContaining(expected), expect.any(Function)));
});

it('check the object', async () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

const schema = {
fields: [
{
component: 'text-field',
name: 'field1',
label: 'field1',
},
{
component: 'text-field',
name: 'field2',
label: 'field2',
condition: {
when: 'field1',
is: 'foo',
then: {
set: (formState) => {
return null;
},
},
else: { visible: true, set: {} },
},
},
],
};

render(<FormRenderer {...initialProps} schema={schema} />);

await userEvent.type(screen.getByLabelText('field1'), 'foo');

await waitFor(() => {
expect(errorSpy).toHaveBeenCalled();
expect(console.error.mock.calls[0][0]).toContain('Received invalid setterValue. Expected object, received: ');
});
});

it('check the getFieldState object', async () => {
const schema = {
fields: [
{
component: 'text-field',
name: 'field1',
label: 'field1',
},
{
component: 'text-field',
name: 'field2',
label: 'field2',
condition: {
when: 'field1',
is: 'foo',
then: {
set: (_formState, getFieldState) => {
return { field2: getFieldState('field1').value };
},
},
else: { visible: true, set: {} },
},
},
],
};
render(<FormRenderer {...initialProps} schema={schema} />);

await userEvent.type(screen.getByLabelText('field1'), 'foo');

await waitFor(() => {
expect(screen.getByLabelText('field2')).toHaveValue('foo');
});
});

describe('reducer', () => {
it('returns default', () => {
const initialState = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import FormRenderer from '@data-driven-forms/react-form-renderer/form-renderer';
import componentTypes from '@data-driven-forms/react-form-renderer/component-types';

import Checkbox from '@data-driven-forms/mui-component-mapper/checkbox';
import TextField from '@data-driven-forms/mui-component-mapper/text-field';
import FormTemplate from '@data-driven-forms/mui-component-mapper/form-template';

const schema = {
fields: [
{
component: componentTypes.TEXT_FIELD,
name: 'firstname',
label: 'Firstname',
description: 'Type Bob to set nickname to Bob',
},
{
component: componentTypes.TEXT_FIELD,
name: 'nickname',
label: 'Nickname',
condition: {
when: 'firstname',
is: 'Bob',
then: {
set: (formState) => {
return { nickname: formState.values.firstname };
},
},
else: { visible: true, set: {} },
},
},
],
};

const componentMapper = {
[componentTypes.CHECKBOX]: Checkbox,
[componentTypes.TEXT_FIELD]: TextField,
};

const SetFunction = () => <FormRenderer FormTemplate={FormTemplate} componentMapper={componentMapper} schema={schema} onSubmit={console.log} />;

export default SetFunction;
Loading