From a947680e03a7fad746fb50399c2c7c608234bf68 Mon Sep 17 00:00:00 2001 From: Julie Prazakova Date: Tue, 4 Oct 2022 11:34:22 +0200 Subject: [PATCH 1/2] fix renderer dev server --- packages/common/config/webpack.config.js | 7 ++++++- packages/react-form-renderer/package.json | 4 +++- yarn.lock | 5 +++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/common/config/webpack.config.js b/packages/common/config/webpack.config.js index 7b4816d33..0633ecdd3 100644 --- a/packages/common/config/webpack.config.js +++ b/packages/common/config/webpack.config.js @@ -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: { diff --git a/packages/react-form-renderer/package.json b/packages/react-form-renderer/package.json index 2ce4306e2..da6bfc150 100644 --- a/packages/react-form-renderer/package.json +++ b/packages/react-form-renderer/package.json @@ -27,7 +27,9 @@ "forms", "javascript" ], - "devDependencies": {}, + "devDependencies": { + "process": "^0.11.10" + }, "dependencies": { "final-form": "^4.20.4", "final-form-arrays": "^3.0.2", diff --git a/yarn.lock b/yarn.lock index f54067c9e..e313a00d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16565,6 +16565,11 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + progress@^2.0.0, progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" From 14de368087d71fa72eed1c07a56344381b9b561f Mon Sep 17 00:00:00 2001 From: Julie Prazakova Date: Tue, 4 Oct 2022 16:02:39 +0200 Subject: [PATCH 2/2] feat(renderer): enable condition set as a function --- packages/react-form-renderer/demo/index.js | 52 ++----- .../src/condition/condition.d.ts | 5 +- .../src/condition/condition.js | 36 ++++- .../default-schema-validator.js | 2 +- .../src/tests/form-renderer/condition.test.js | 144 ++++++++++++++++++ .../components/conditions/set-function.js | 42 +++++ .../react-renderer-demo/src/next.config.js | 12 +- .../src/pages/schema/condition-set.md | 16 ++ 8 files changed, 256 insertions(+), 53 deletions(-) create mode 100644 packages/react-renderer-demo/src/examples/components/conditions/set-function.js diff --git a/packages/react-form-renderer/demo/index.js b/packages/react-form-renderer/demo/index.js index 0158fcd8f..28f428583 100644 --- a/packages/react-form-renderer/demo/index.js +++ b/packages/react-form-renderer/demo/index.js @@ -7,41 +7,27 @@ 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: {} }, }, }, ], @@ -49,17 +35,7 @@ const schema = { const App = () => { return (
- +
); }; diff --git a/packages/react-form-renderer/src/condition/condition.d.ts b/packages/react-form-renderer/src/condition/condition.d.ts index bacfefb4c..4213959cb 100644 --- a/packages/react-form-renderer/src/condition/condition.d.ts +++ b/packages/react-form-renderer/src/condition/condition.d.ts @@ -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>, getFieldState:FormApi["getFieldState"]) => object) ; // TO DO specify this } export type InnerWhenFunction = (currentField: string) => string; diff --git a/packages/react-form-renderer/src/condition/condition.js b/packages/react-form-renderer/src/condition/condition.js index ae1e5da60..5e87c457b 100644 --- a/packages/react-form-renderer/src/condition/condition.js +++ b/packages/react-form-renderer/src/condition/condition.js @@ -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': @@ -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) => { @@ -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); + } + } }); } }); @@ -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]), }), }; diff --git a/packages/react-form-renderer/src/default-schema-validator/default-schema-validator.js b/packages/react-form-renderer/src/default-schema-validator/default-schema-validator.js index 55492339f..4bb3de37e 100644 --- a/packages/react-form-renderer/src/default-schema-validator/default-schema-validator.js +++ b/packages/react-form-renderer/src/default-schema-validator/default-schema-validator.js @@ -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)}. diff --git a/packages/react-form-renderer/src/tests/form-renderer/condition.test.js b/packages/react-form-renderer/src/tests/form-renderer/condition.test.js index 7aa5fc4ab..ff8ca2713 100644 --- a/packages/react-form-renderer/src/tests/form-renderer/condition.test.js +++ b/packages/react-form-renderer/src/tests/form-renderer/condition.test.js @@ -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(); + 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(); + + 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(); + + 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(); + + await userEvent.type(screen.getByLabelText('field1'), 'foo'); + + await waitFor(() => { + expect(screen.getByLabelText('field2')).toHaveValue('foo'); + }); + }); + describe('reducer', () => { it('returns default', () => { const initialState = { diff --git a/packages/react-renderer-demo/src/examples/components/conditions/set-function.js b/packages/react-renderer-demo/src/examples/components/conditions/set-function.js new file mode 100644 index 000000000..1889f42e6 --- /dev/null +++ b/packages/react-renderer-demo/src/examples/components/conditions/set-function.js @@ -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 = () => ; + +export default SetFunction; diff --git a/packages/react-renderer-demo/src/next.config.js b/packages/react-renderer-demo/src/next.config.js index ff6e7f99e..24dc5737c 100644 --- a/packages/react-renderer-demo/src/next.config.js +++ b/packages/react-renderer-demo/src/next.config.js @@ -57,16 +57,16 @@ module.exports = withBundleAnalyzer( ...config.resolve.alias, ...(process.env.DEPLOY === 'true' ? { - "@emotion/react": path.resolve(__dirname, '../node_modules/@emotion/react'), - "@emotion/server": path.resolve(__dirname, '../node_modules/@emotion/server'), - "@emotion/styled": path.resolve(__dirname, '../node_modules/@emotion/styled'), + '@emotion/react': path.resolve(__dirname, '../node_modules/@emotion/react'), + '@emotion/server': path.resolve(__dirname, '../node_modules/@emotion/server'), + '@emotion/styled': path.resolve(__dirname, '../node_modules/@emotion/styled'), } : { react: path.resolve(__dirname, '../../../node_modules/react'), 'react-dom': path.resolve(__dirname, '../../../node_modules/react-dom'), - "@emotion/react": path.resolve(__dirname, '../../../node_modules/@emotion/react'), - "@emotion/server": path.resolve(__dirname, '../../../node_modules/@emotion/server'), - "@emotion/styled": path.resolve(__dirname, '../../../node_modules/@emotion/styled'), + '@emotion/react': path.resolve(__dirname, '../../../node_modules/@emotion/react'), + '@emotion/server': path.resolve(__dirname, '../../../node_modules/@emotion/server'), + '@emotion/styled': path.resolve(__dirname, '../../../node_modules/@emotion/styled'), }), '@docs/doc-components': path.resolve(__dirname, './doc-components'), diff --git a/packages/react-renderer-demo/src/pages/schema/condition-set.md b/packages/react-renderer-demo/src/pages/schema/condition-set.md index 962850a2c..13a5c9fc9 100644 --- a/packages/react-renderer-demo/src/pages/schema/condition-set.md +++ b/packages/react-renderer-demo/src/pages/schema/condition-set.md @@ -1,4 +1,5 @@ import DocPage from '@docs/doc-page'; +import CodeExample from '@docs/code-example' @@ -6,6 +7,8 @@ import DocPage from '@docs/doc-page'; Setter allows to change form values according to selected values in different fields. +## Set object + ```jsx // Single value condition: { when: 'x', is: 'y', then: { set: { [field]: value } } } @@ -22,4 +25,17 @@ Set is a object consists of field names as keys and values as values. You can ch When the field containing a condition has some defined initial value, the setter is not triggered until the setter is retriggered with a different value. + + +## Set function + +To enable dynamic set action you can define set as a function. + +```TS +type set = (formState: FormState, getFieldState: ((fieldName: string) => FieldState)): void +``` + + + +