From a83d55da1bdfa894648a587a1c6eedda317a6770 Mon Sep 17 00:00:00 2001 From: Nikita Zolotykh Date: Tue, 21 Nov 2023 19:20:57 +0300 Subject: [PATCH] fix: fix state updates by mutators --- src/lib/core/components/Form/Controller.tsx | 17 +---- src/lib/core/components/Form/hooks/index.tsx | 1 + .../core/components/Form/hooks/useField.tsx | 62 ++++++++++++++++++- .../core/components/Form/hooks/useMutators.ts | 30 ++++++++- src/lib/core/components/Form/hooks/useSpec.ts | 50 +++++++++++++++ .../core/components/Form/types/mutators.ts | 19 +++++- 6 files changed, 158 insertions(+), 21 deletions(-) create mode 100644 src/lib/core/components/Form/hooks/useSpec.ts diff --git a/src/lib/core/components/Form/Controller.tsx b/src/lib/core/components/Form/Controller.tsx index a7ced1fd..eb855f3c 100644 --- a/src/lib/core/components/Form/Controller.tsx +++ b/src/lib/core/components/Form/Controller.tsx @@ -1,10 +1,7 @@ -import React from 'react'; - import _ from 'lodash'; import {Spec} from '../../types'; -import {EMPTY_MUTATOR} from './constants'; import { useComponents, useControllerMirror, @@ -12,6 +9,7 @@ import { useField, useRender, useSearch, + useSpec, useValidate, } from './hooks'; import {ControllerMirror, FieldValue, ValidateError} from './types'; @@ -38,17 +36,7 @@ export const Controller = ({ parentOnUnmount, }: ControllerProps) => { const {tools, mutators, __mirror} = useDynamicFormsCtx(); - - const spec = React.useMemo(() => { - const specMutator = _.get(mutators.spec, name, EMPTY_MUTATOR); - - if (specMutator !== EMPTY_MUTATOR) { - return _.merge(_spec, specMutator); - } - - return _spec; - }, [_spec, mutators.spec, name]); - + const spec = useSpec({name, spec: _spec, mutators}); const {inputEntity, Layout} = useComponents(spec); const render = useRender({name, spec, inputEntity, Layout}); const validate = useValidate(spec); @@ -57,6 +45,7 @@ export const Controller = ({ initialValue: _.get(tools.initialValue, name), value, spec, + originalSpec: _spec, validate, tools, parentOnChange, diff --git a/src/lib/core/components/Form/hooks/index.tsx b/src/lib/core/components/Form/hooks/index.tsx index 34cd4d15..d4b8d40f 100644 --- a/src/lib/core/components/Form/hooks/index.tsx +++ b/src/lib/core/components/Form/hooks/index.tsx @@ -16,4 +16,5 @@ export * from './useMonaco'; export * from './useSearchStore'; export * from './useSearchContext'; export * from './useSearch'; +export * from './useSpec'; export * from './useCreateSearchContext'; diff --git a/src/lib/core/components/Form/hooks/useField.tsx b/src/lib/core/components/Form/hooks/useField.tsx index e64b5b66..fd28e461 100644 --- a/src/lib/core/components/Form/hooks/useField.tsx +++ b/src/lib/core/components/Form/hooks/useField.tsx @@ -2,7 +2,7 @@ import React from 'react'; import _ from 'lodash'; -import {isArraySpec, isNumberSpec, isObjectSpec} from '../../../helpers'; +import {isArraySpec, isCorrectSpec, isNumberSpec, isObjectSpec} from '../../../helpers'; import {Spec} from '../../../types'; import {EMPTY_MUTATOR, OBJECT_ARRAY_CNT, OBJECT_ARRAY_FLAG} from '../constants'; import { @@ -13,18 +13,23 @@ import { FieldRenderProps, FieldValue, ValidateError, + ValidatorsMap, } from '../types'; import { isArrayItem, + isCorrectConfig, isErrorMutatorCorrect, isValueMutatorCorrect, transformArrIn, transformArrOut, } from '../utils'; +import {useDynamicFormsCtx} from './useDynamicFormsCtx'; + export interface UseFieldProps { name: string; spec: SpecType; + originalSpec: SpecType; initialValue: Value; value: Value; validate?: (value?: Value) => ValidateError; @@ -39,10 +44,58 @@ export interface UseFieldProps parentOnUnmount: ((childName: string) => void) | null; mutators: DynamicFormMutators; } +// TODO: remove +const useExtraValidator = ({ + name, + spec, +}: { + name: string; + spec: SpecType; +}) => { + const {mutators, config} = useDynamicFormsCtx(); + + const nextSpec = (() => { + const specMutator = _.get(mutators.spec, name, EMPTY_MUTATOR); + + if (specMutator !== EMPTY_MUTATOR) { + return _.merge(_.cloneDeep(spec), specMutator); + } + + return spec; + })(); + const validator = (() => { + if (isCorrectConfig(config) && isCorrectSpec(nextSpec)) { + const {validators} = config[nextSpec.type] as unknown as { + validators: ValidatorsMap; + }; + + if (validators) { + if ( + (!_.isString(nextSpec.validator) || !nextSpec.validator.length) && + _.isFunction(validators.base) + ) { + return (value?: Value) => validators.base(nextSpec, value); + } + + if ( + _.isString(nextSpec.validator) && + _.isFunction(validators[nextSpec.validator]) + ) { + return (value?: Value) => validators[nextSpec.validator!]!(nextSpec, value); + } + } + } + + return; + })(); + + return validator; +}; export const useField = ({ name, spec, + originalSpec, initialValue, value: externalValue, validate: propsValidate, @@ -52,6 +105,7 @@ export const useField = ({ mutators, }: UseFieldProps): FieldRenderProps => { const firstRenderRef = React.useRef(true); + const extraValidator = useExtraValidator({name, spec: originalSpec}); const validate = React.useCallback( (value: Value) => propsValidate?.(transformArrOut(value)), @@ -112,10 +166,12 @@ export const useField = ({ valOrSetter: Value | ((currentValue: Value) => Value), childErrors?: Record, errorMutator?: ValidateError, + extraValidator?: ReturnType>, ) => { setState((state) => { const _value = _.isFunction(valOrSetter) ? valOrSetter(state.value) : valOrSetter; - const error = validate?.(_value) || errorMutator; + const error = + (extraValidator ? extraValidator(_value) : validate?.(_value)) || errorMutator; let value = transformArrIn(_value); if (isNumberSpec(spec) && !error) { @@ -298,7 +354,7 @@ export const useField = ({ valueMutator !== state.value && valueMutator !== EMPTY_MUTATOR ) { - onLocalChange(valueMutator, undefined, errorMutator); + onLocalChange(valueMutator, undefined, errorMutator, extraValidator); } else if (state.error !== errorMutator && !(state.error && !errorMutator)) { setState({...state, error: errorMutator}); (parentOnChange ? parentOnChange : tools.onChange)(name, state.value, { diff --git a/src/lib/core/components/Form/hooks/useMutators.ts b/src/lib/core/components/Form/hooks/useMutators.ts index ac9962d8..52a52b1b 100644 --- a/src/lib/core/components/Form/hooks/useMutators.ts +++ b/src/lib/core/components/Form/hooks/useMutators.ts @@ -2,7 +2,7 @@ import React from 'react'; import _ from 'lodash'; -import {DynamicFormMutators} from '../types'; +import {DynamicFormMutators, SpecMutator} from '../types'; export const useMutators = (externalMutators?: DynamicFormMutators) => { const firstRenderRef = React.useRef(true); @@ -10,11 +10,37 @@ export const useMutators = (externalMutators?: DynamicFormMutators) => { const mutateDFState = React.useCallback( (mutators: DynamicFormMutators) => { + const mergeSpec = (a: Record, b: Record) => { + const result = _.cloneDeep(a); + + const getKeys = (parent: any): string[][] => { + if (_.isObjectLike(parent)) { + return _.keys(parent).reduce((acc: string[][], parentKey) => { + const childKeys = getKeys(parent[parentKey]); + + return [ + ...acc, + ...(childKeys.length ? [] : [[parentKey]]), + ...childKeys.map((childKey) => [parentKey, ...childKey]), + ]; + }, []); + } + + return []; + }; + + getKeys(b).forEach((key) => { + _.set(result, key, _.get(b, key)); + }); + + return result; + }; + setStore((store) => ({ ...store, ...(mutators.errors ? {errors: _.merge(store.errors, mutators.errors)} : {}), ...(mutators.values ? {values: _.merge(store.values, mutators.values)} : {}), - ...(mutators.spec ? {spec: _.merge(store.spec, mutators.spec)} : {}), + ...(mutators.spec ? {spec: mergeSpec(store.spec || {}, mutators.spec)} : {}), })); }, [setStore], diff --git a/src/lib/core/components/Form/hooks/useSpec.ts b/src/lib/core/components/Form/hooks/useSpec.ts new file mode 100644 index 00000000..b21c0302 --- /dev/null +++ b/src/lib/core/components/Form/hooks/useSpec.ts @@ -0,0 +1,50 @@ +import React from 'react'; + +import _ from 'lodash'; + +import {Spec} from '../../../types'; +import {EMPTY_MUTATOR} from '../constants'; +import {DynamicFormMutators} from '../types'; + +export interface UseSpecParams { + name: string; + spec: Spec; + mutators: DynamicFormMutators; +} + +export const useSpec = ({spec: _spec, mutators, name}: UseSpecParams) => { + const firstRenderRef = React.useRef(true); + const [spec, setSpec] = React.useState(() => { + const specMutator = _.get(mutators.spec, name, EMPTY_MUTATOR); + + if (specMutator !== EMPTY_MUTATOR) { + return _.merge(_.cloneDeep(_spec), specMutator); + } + + return _spec; + }); + + React.useEffect(() => { + if (!firstRenderRef.current) { + const specMutator = _.get(mutators.spec, name, EMPTY_MUTATOR); + + if (specMutator === EMPTY_MUTATOR) { + if (!_.isEqual(spec, _spec)) { + setSpec(_spec); + } + } else { + const nextSpec = _.merge(_.cloneDeep(_spec), specMutator); + + if (!_.isEqual(spec, nextSpec)) { + setSpec(nextSpec); + } + } + } + }, [_spec, mutators, name]); + + React.useEffect(() => { + firstRenderRef.current = false; + }, []); + + return spec; +}; diff --git a/src/lib/core/components/Form/types/mutators.ts b/src/lib/core/components/Form/types/mutators.ts index d043ddc7..8afe0544 100644 --- a/src/lib/core/components/Form/types/mutators.ts +++ b/src/lib/core/components/Form/types/mutators.ts @@ -1,9 +1,24 @@ -import {FormValue, Spec} from '../../../types'; +import { + ArraySpec, + BooleanSpec, + FormValue, + NumberSpec, + ObjectSpec, + StringSpec, +} from '../../../types'; import {BaseValidateError} from './'; +export type SpecMutator = Partial< + | (Omit & {viewSpec: Partial}) + | (Omit & {viewSpec: Partial}) + | (Omit & {viewSpec: Partial}) + | (Omit & {viewSpec: Partial}) + | (Omit & {viewSpec: Partial}) +>; + export interface DynamicFormMutators { errors?: Record; values?: Record; - spec?: Record>; + spec?: Record; }