From f27205b411e3e24b7e52cc904ec4c75503216d22 Mon Sep 17 00:00:00 2001 From: Pierre Huyghe Date: Thu, 24 Feb 2022 00:36:04 +0100 Subject: [PATCH] refactor(libs/form-builder): remove lodash usage (#59) --- libs/form-builder/package.json | 1 - .../src/lib/__tests__/fixtures.tsx | 3 +- .../src/lib/components/stepper.component.tsx | 5 +- libs/form-builder/src/lib/formBuilder.tsx | 69 +++++++++---------- .../src/lib/hooks/useAutoFocus.hook.ts | 5 +- libs/form-builder/src/lib/types.ts | 2 +- libs/form-builder/src/lib/utils/error.util.ts | 9 ++- .../src/lib/utils/getSchemaInfo.util.ts | 21 +++--- .../lib/utils/handleFormBuilderError.util.ts | 19 ++--- .../src/lib/utils/object.utils.ts | 1 + .../src/lib/utils/validation.utils.ts | 30 ++++---- libs/form-context/package.json | 5 +- .../src/lib/__tests__/forms.selectors.spec.ts | 6 +- libs/form-context/src/lib/forms.reducer.ts | 39 ++++++----- libs/form-context/src/lib/forms.selectors.ts | 14 ++-- libs/form-editor/package.json | 16 ++--- libs/form-redux/package.json | 5 +- .../src/lib/__tests__/forms.reducer.spec.ts | 3 +- .../src/lib/__tests__/forms.selectors.spec.ts | 10 +-- libs/form-redux/src/lib/forms.reducer.ts | 33 +++++---- libs/form-redux/src/lib/forms.selectors.ts | 28 ++++---- libs/form-validation-rule-list/package.json | 1 - .../validationRuleList.component.tsx | 18 +++-- .../src/lib/getValidationRulesHints.ts | 12 ++-- .../form-validation-rule-list/src/lib/rule.ts | 5 +- 25 files changed, 186 insertions(+), 174 deletions(-) create mode 100644 libs/form-builder/src/lib/utils/object.utils.ts diff --git a/libs/form-builder/package.json b/libs/form-builder/package.json index 4bc7a82..1c41c68 100644 --- a/libs/form-builder/package.json +++ b/libs/form-builder/package.json @@ -6,7 +6,6 @@ "react-hook-form": "7.27.0" }, "peerDependencies": { - "lodash": "4.17.21", "react": "17.0.2" }, "devDependencies": { diff --git a/libs/form-builder/src/lib/__tests__/fixtures.tsx b/libs/form-builder/src/lib/__tests__/fixtures.tsx index e3e79b4..175cad3 100644 --- a/libs/form-builder/src/lib/__tests__/fixtures.tsx +++ b/libs/form-builder/src/lib/__tests__/fixtures.tsx @@ -1,5 +1,4 @@ import { Dictionary } from '../types'; -import _ from 'lodash'; import { FormSchema } from '../types'; const makeStep = ({ fieldsById, stepId, label }: { fieldsById: string[]; stepId: string; label: string }) => ({ @@ -69,4 +68,4 @@ export const CORRECT_DICTIONARY: Dictionary = { ), }; -export const typesAllowed = _.keys(CORRECT_DICTIONARY); +export const typesAllowed = Object.keys(CORRECT_DICTIONARY); diff --git a/libs/form-builder/src/lib/components/stepper.component.tsx b/libs/form-builder/src/lib/components/stepper.component.tsx index 024c257..69d8220 100644 --- a/libs/form-builder/src/lib/components/stepper.component.tsx +++ b/libs/form-builder/src/lib/components/stepper.component.tsx @@ -1,13 +1,12 @@ import * as React from 'react'; -import _ from 'lodash'; export interface StepperProps { - children?: React.ReactElement[] | React.ReactElement; + children?: React.ReactElement[] | null; currentStepIndex: number; } export const Stepper = ({ children, currentStepIndex }: StepperProps) => { - const child = _.get(children, currentStepIndex, null); + const child = children?.[currentStepIndex] || null; // eslint-disable-next-line react/jsx-no-useless-fragment return <>{child}; diff --git a/libs/form-builder/src/lib/formBuilder.tsx b/libs/form-builder/src/lib/formBuilder.tsx index 0da391a..d2c4e61 100644 --- a/libs/form-builder/src/lib/formBuilder.tsx +++ b/libs/form-builder/src/lib/formBuilder.tsx @@ -11,8 +11,6 @@ import { } from 'react-hook-form'; import { DevTool } from '@hookform/devtools'; -import _ from 'lodash'; - import { Dictionary, ExtraValidation, FormSchema } from './types'; import { useAutoFocus } from './hooks/useAutoFocus.hook'; import { useIsFormStepValid } from './hooks/useIsFormStepValid'; @@ -25,8 +23,10 @@ import { FormField } from './components/formField.component'; import { SubmitField } from './components/submitField.component'; import { getFieldRules, FieldRules } from './utils/validation.utils'; import { filterDependentsFieldsById } from './utils/conditionalFields.utils'; +import { isEmpty } from './utils/object.utils'; const EMPTY_OBJECT = {} as const; +const NOOP = () => null; export interface FormBuilderProps { formId: string; @@ -48,7 +48,7 @@ export function FormBuilder({ schema, dictionary, onSubmit, - onNextStep = _.noop, + onNextStep = NOOP, extraValidation, defaultValues, behavior = 'onChange', @@ -71,9 +71,9 @@ export function FormBuilder({ defaultValues, }); - const isPreFilled = !_.isEmpty(defaultValues); + const isPreFilled = defaultValues && typeof defaultValues === 'object' && Object.keys(defaultValues).length > 0; - const typesAllowed = React.useMemo(() => _.keys(dictionary), [dictionary]); + const typesAllowed = React.useMemo(() => Object.keys(dictionary || EMPTY_OBJECT), [dictionary]); const { fields, fieldsById, stepsById, submitLabel } = React.useMemo( () => getSchemaInfo(schema, typesAllowed, currentStepIndex), @@ -90,18 +90,14 @@ export function FormBuilder({ const validationRulesById = React.useMemo( () => - _.reduce( - fieldsById, - (accumulator, fieldId) => { - const validation = _.get(fields, [fieldId, 'validation'], EMPTY_OBJECT); - - return { - ...accumulator, - [fieldId]: getFieldRules({ validation, extraValidation }), - }; - }, - {} as { [key: string]: FieldRules }, - ), + fieldsById.reduce((accumulator, fieldId) => { + const validation = fields?.[fieldId]?.validation || EMPTY_OBJECT; + + return { + ...accumulator, + [fieldId]: getFieldRules({ validation, extraValidation }), + }; + }, {} as { [key: string]: FieldRules }), [extraValidation, fields, fieldsById], ); @@ -127,7 +123,7 @@ export function FormBuilder({ // Displays nice and informative errors in dev mode if (debug) handleFormBuilderError(typesAllowed, schema, dictionary); - if (_.isEmpty(schema) || _.isEmpty(dictionary) || typeof onSubmit !== 'function') return null; + if (isEmpty(schema) || isEmpty(dictionary) || typeof onSubmit !== 'function') return null; return ( <> @@ -138,9 +134,9 @@ export function FormBuilder({ onSubmit={handleSubmit(onSubmit)} > - {_.map(stepsById, (stepId) => ( + {stepsById?.map((stepId) => ( - {_.map(filteredFields, (fieldId) => { + {filteredFields?.map((fieldId) => { const { type, id, defaultValue, meta, validation } = fields[fieldId]; return ( @@ -150,21 +146,24 @@ export function FormBuilder({ control={control} defaultValue={defaultValue} rules={validationRulesById[fieldId]} - render={({ field }) => ( - - )} + render={({ field }) => { + const { ref, ...fieldRest } = field; + return ( + + ); + }} /> ); })} diff --git a/libs/form-builder/src/lib/hooks/useAutoFocus.hook.ts b/libs/form-builder/src/lib/hooks/useAutoFocus.hook.ts index 38eb8cf..258a7b1 100644 --- a/libs/form-builder/src/lib/hooks/useAutoFocus.hook.ts +++ b/libs/form-builder/src/lib/hooks/useAutoFocus.hook.ts @@ -1,5 +1,4 @@ import { useEffect } from 'react'; -import _ from 'lodash'; import { FieldValues, UseFormSetFocus } from 'react-hook-form'; import { FormSchema } from '../types'; @@ -11,8 +10,8 @@ export interface UseAutoFocusArgs { export const useAutoFocus = ({ currentStepIndex, schema, setFocus }: UseAutoFocusArgs) => { useEffect(() => { - const currentStepId = _.get(schema, `stepsById.${currentStepIndex}`); - const firstFieldIdInStep = _.get(schema, ['steps', currentStepId, 'fieldsById', 0]); + const currentStepId = schema?.stepsById?.[currentStepIndex]; + const firstFieldIdInStep = schema?.steps?.[currentStepId]?.fieldsById?.[0]; try { setFocus(firstFieldIdInStep); diff --git a/libs/form-builder/src/lib/types.ts b/libs/form-builder/src/lib/types.ts index 4afdda7..506d0e4 100644 --- a/libs/form-builder/src/lib/types.ts +++ b/libs/form-builder/src/lib/types.ts @@ -62,5 +62,5 @@ export interface Dictionary { } export interface ExtraValidation { - [key: string]: (value?: any) => (input?: any) => boolean | string | Promise; + [key: string]: (value?: any) => (input?: any) => boolean | string | undefined | Promise; } diff --git a/libs/form-builder/src/lib/utils/error.util.ts b/libs/form-builder/src/lib/utils/error.util.ts index dc31b04..79f47bf 100644 --- a/libs/form-builder/src/lib/utils/error.util.ts +++ b/libs/form-builder/src/lib/utils/error.util.ts @@ -1,4 +1,3 @@ -import _ from 'lodash'; import { FieldErrors, DefaultValues, FieldValues } from 'react-hook-form'; import { DEFAULT_RULES_NAMES } from '../constants'; import { DirtyFields, FormSchema } from '../types'; @@ -12,8 +11,8 @@ export const getFieldsToCheckByStep = ({ schema: FormSchema; currentStepIndex: number; }): string[] | readonly [] => { - const currentStepId = _.get(schema, ['stepsById', currentStepIndex]); - const fieldsToCheck = _.get(schema, ['steps', currentStepId, 'fieldsById'], EMPTY_ARRAY); + const currentStepId = schema?.stepsById?.[currentStepIndex]; + const fieldsToCheck = schema?.steps?.[currentStepId]?.fieldsById || EMPTY_ARRAY; return fieldsToCheck; }; @@ -22,7 +21,7 @@ export const isFieldInError = ({ fieldToCheck, errors }: { fieldToCheck: string; !!(errors && errors[fieldToCheck]); export const isFieldRequired = ({ schema, fieldToCheck }: { schema: FormSchema; fieldToCheck: string }) => - _.get(schema, ['fields', fieldToCheck, 'validation', DEFAULT_RULES_NAMES.required], false); + schema?.fields?.[fieldToCheck]?.validation?.[DEFAULT_RULES_NAMES.required]; export const isFieldNotDirtyAndEmpty = ({ fieldToCheck, @@ -32,7 +31,7 @@ export const isFieldNotDirtyAndEmpty = ({ fieldToCheck: string; dirtyFields: DirtyFields; defaultValues?: DefaultValues; -}) => !_.get(dirtyFields, fieldToCheck) && !_.get(defaultValues, fieldToCheck); +}) => !dirtyFields?.[fieldToCheck] && !defaultValues?.[fieldToCheck]; export const isStepInError = ({ fieldsToCheckByStep, diff --git a/libs/form-builder/src/lib/utils/getSchemaInfo.util.ts b/libs/form-builder/src/lib/utils/getSchemaInfo.util.ts index 56bd2da..e87c4b6 100644 --- a/libs/form-builder/src/lib/utils/getSchemaInfo.util.ts +++ b/libs/form-builder/src/lib/utils/getSchemaInfo.util.ts @@ -1,13 +1,12 @@ -import _ from 'lodash'; import { SUBMIT_FIELD_TYPE } from '../constants'; import { FormFields, FormSchema } from '../types'; -const EMPTY_ARRAY = [] as string[]; -const EMPTY_OBJECT = {} as any; +const EMPTY_ARRAY = [] as const; +const EMPTY_OBJECT = {} as const; export const sanitizeFieldsById = (fieldsById: string[], fields: FormFields, typesAllowed: string[]): string[] => - _.filter(fieldsById, (fieldId) => { - const type = _.get(fields, [fieldId, 'type']); + fieldsById.filter((fieldId) => { + const type = fields?.[fieldId]?.type; return typesAllowed.includes(type) && type !== SUBMIT_FIELD_TYPE; }); @@ -20,12 +19,12 @@ export interface SchemaInfo { } export const getSchemaInfo = (schema: FormSchema, typesAllowed: string[], currentStepIndex: number): SchemaInfo => { - const steps = _.get(schema, 'steps'); - const stepsById = _.get(schema, 'stepsById', EMPTY_ARRAY); - const stepId = _.get(stepsById, currentStepIndex); - const fieldsById = _.get(steps, [stepId, 'fieldsById'], EMPTY_ARRAY); - const submitLabel = _.get(steps, [stepId, 'submit', 'label']); - const fields = _.get(schema, 'fields', EMPTY_OBJECT); + const steps = schema?.steps; + const stepsById = schema?.stepsById || EMPTY_ARRAY; + const stepId = stepsById?.[currentStepIndex]; + const fieldsById = steps?.[stepId]?.fieldsById || EMPTY_ARRAY; + const submitLabel = steps?.[stepId]?.submit?.label; + const fields = schema?.fields || EMPTY_OBJECT; return { fields, diff --git a/libs/form-builder/src/lib/utils/handleFormBuilderError.util.ts b/libs/form-builder/src/lib/utils/handleFormBuilderError.util.ts index 9acec5d..bfaf3ec 100644 --- a/libs/form-builder/src/lib/utils/handleFormBuilderError.util.ts +++ b/libs/form-builder/src/lib/utils/handleFormBuilderError.util.ts @@ -1,11 +1,13 @@ -import _ from 'lodash'; import { FormBuilderError } from './formBuilderError.utils'; import { SUBMIT_FIELD_TYPE } from '../constants'; import { Dictionary, FormSchema } from '../types'; +const EMPTY_OBJECT = {} as const; +const EMPTY_ARRAY = [] as const; + export const handleFormBuilderError = (typesAllowed: string[], schema: FormSchema, dictionary: Dictionary) => { - const invalidTypesInSchema = _.filter( - schema.fields, + const fieldValues = Object.values(schema?.fields || EMPTY_OBJECT); + const invalidTypesInSchema = fieldValues.filter( ({ type }) => !typesAllowed.includes(type) || type === SUBMIT_FIELD_TYPE, ); @@ -19,20 +21,21 @@ export const handleFormBuilderError = (typesAllowed: string[], schema: FormSchem ); } - const steps = _.get(schema, 'steps'); + const steps = schema?.steps || EMPTY_OBJECT; + const isStepsNonNullObject = steps && typeof steps === 'object'; - if (!_.isObject(steps) || _.isEmpty(steps)) { + if (!isStepsNonNullObject || Object.keys(steps).length === 0) { throw new FormBuilderError( `The form's schema must contain a map of steps by id. Found: \n${JSON.stringify(steps, null, 2)}`, ); } - const stepsById = _.get(schema, 'stepsById'); + const stepsById = schema?.stepsById || EMPTY_ARRAY; - if (_.keys(steps).length !== stepsById.length) { + if (Object.keys(steps).length !== stepsById.length) { throw new FormBuilderError( `The form's schema must contain as many steps entries as steps ids. Found: \n${JSON.stringify( - { steps: _.keys(steps).length, stepsById: stepsById.length }, + { steps: Object.keys(steps).length, stepsById: stepsById.length }, null, 2, )}`, diff --git a/libs/form-builder/src/lib/utils/object.utils.ts b/libs/form-builder/src/lib/utils/object.utils.ts new file mode 100644 index 0000000..9828132 --- /dev/null +++ b/libs/form-builder/src/lib/utils/object.utils.ts @@ -0,0 +1 @@ +export const isEmpty = (obj: unknown) => !obj || typeof obj !== 'object' || Object.keys(obj || {}).length === 0; diff --git a/libs/form-builder/src/lib/utils/validation.utils.ts b/libs/form-builder/src/lib/utils/validation.utils.ts index a33f9c5..f1bc18f 100644 --- a/libs/form-builder/src/lib/utils/validation.utils.ts +++ b/libs/form-builder/src/lib/utils/validation.utils.ts @@ -1,13 +1,14 @@ import { RegisterOptions } from 'react-hook-form'; -import _ from 'lodash'; import { DEFAULT_RULES_NAMES } from '../constants'; import { ExtraValidation, Validations } from '../types'; +const EMPTY_OBJECT = {} as const; + export const handleValidateErrorMessage = - (validate: (...args: any[]) => boolean | undefined, message: string) => - async (...args: any[]) => { - const result = await validate(...args); + (validate: (input: any) => boolean | string | undefined | Promise, message: string) => + async (input: any): Promise => { + const result = await validate(input); return result || message; }; @@ -21,23 +22,24 @@ export interface FieldRules extends RegisterOptions { validate?: { [key: string]: (value?: any) => Promise | boolean }; } -export const getFieldRules = ({ validation, extraValidation }: GetFieldRulesArgs): FieldRules => { - const hookFormRules = _.reduce( - validation, - (acc, { key, ...rest }) => (_.includes(DEFAULT_RULES_NAMES, key) ? { ...acc, [key]: rest } : acc), - {}, +export const getFieldRules = ({ + validation = EMPTY_OBJECT, + extraValidation = EMPTY_OBJECT, +}: GetFieldRulesArgs): FieldRules => { + const hookFormRules = Object.values(validation).reduce( + (acc, { key, ...rest }) => (DEFAULT_RULES_NAMES?.[key] ? { ...acc, [key]: rest } : acc), + EMPTY_OBJECT, ); - const extraRules = _.reduce( - validation, + const extraRules = Object.values(validation).reduce( (acc, { key, value, message }) => - _.includes(DEFAULT_RULES_NAMES, key) || (extraValidation && !extraValidation[key]) + DEFAULT_RULES_NAMES?.[key] || (extraValidation && !extraValidation[key]) ? acc : { ...acc, - [key]: handleValidateErrorMessage(_.invoke(extraValidation, key, value), message), + [key]: handleValidateErrorMessage(extraValidation?.[key]?.(value), message), }, - {}, + EMPTY_OBJECT, ); const hasExtraRules = !!Object.keys(extraRules).length; diff --git a/libs/form-context/package.json b/libs/form-context/package.json index e2546d3..9cbed46 100644 --- a/libs/form-context/package.json +++ b/libs/form-context/package.json @@ -4,9 +4,8 @@ "peerDependencies": { "react": "17.0.2", "@bedrockstreaming/form-builder": "0.8.1", - "lodash": "4.17.21", - "react-hook-form": "7.27.0", - "@hookform/devtools": "4.0.2" + "@hookform/devtools": "4.0.2", + "react-hook-form": "7.27.0" }, "devDependencies": { "deep-freeze": "0.0.1" diff --git a/libs/form-context/src/lib/__tests__/forms.selectors.spec.ts b/libs/form-context/src/lib/__tests__/forms.selectors.spec.ts index 5343ccf..3d03e7d 100644 --- a/libs/form-context/src/lib/__tests__/forms.selectors.spec.ts +++ b/libs/form-context/src/lib/__tests__/forms.selectors.spec.ts @@ -15,19 +15,19 @@ const state = { describe('forms.selectors', () => { describe('getFormData', () => { it('should retrieve data from state input', () => { - expect(getFormData(formId)(state)).toBe(state.foo.data); + expect(getFormData(formId)(state)).toEqual(state.foo.data); }); }); describe('getCurrentStepIndex', () => { it('should retrieve currentStepIndex from state input', () => { - expect(getCurrentStepIndex(formId)(state)).toBe(state.foo.currentStepIndex); + expect(getCurrentStepIndex(formId)(state)).toEqual(state.foo.currentStepIndex); }); }); describe('isLastStep', () => { it('should retrieve isLastStep property', () => { - expect(isLastStep(formId)(state)).toBe(state.foo.isLastStep); + expect(isLastStep(formId)(state)).toEqual(state.foo.isLastStep); }); }); }); diff --git a/libs/form-context/src/lib/forms.reducer.ts b/libs/form-context/src/lib/forms.reducer.ts index 0ab405c..1ef1e7e 100644 --- a/libs/form-context/src/lib/forms.reducer.ts +++ b/libs/form-context/src/lib/forms.reducer.ts @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import { FormSchema } from '@bedrockstreaming/form-builder'; import { FieldValues } from 'react-hook-form'; import { PREVIOUS_STEP, NEXT_STEP, UPDATE_FORM_DATA, INIT_FORM, RESET_FORM, SET_STEP } from './forms.actions'; @@ -14,7 +14,12 @@ export interface DefaultFormState { export interface FormAction { type: string; - [key: string]: any; + formId: string; + data?: { + [key: string]: unknown; + }; + schema?: FormSchema; + [key: string]: unknown; } const defaultFormState = { @@ -24,8 +29,8 @@ const defaultFormState = { data: {}, }; -const DEFAULT_OBJECT = {}; -export const initialState = {} as any; +const DEFAULT_OBJECT = {} as const; +export const initialState = {} as DefaultFormState; const checkFormId = ({ formId }: FormAction) => !!formId; const checkFormExist = ({ formId }: FormAction, state: DefaultFormState) => !!state[formId]; @@ -38,7 +43,7 @@ export const reducer = (state = initialState, action: FormAction) => { return state; } - const stepsById = _.get(action, ['schema', 'stepsById'], [] as string[]); + const stepsById = action?.schema?.stepsById || ([] as string[]); const currentFormState = { ...defaultFormState, stepsCount: stepsById.length, @@ -59,9 +64,9 @@ export const reducer = (state = initialState, action: FormAction) => { return state; } - const formState = _.get(state, action.formId, DEFAULT_OBJECT); - const currentStepIndex = _.get(state, [action.formId, 'currentStepIndex'], 0); - const stepsCount = _.get(state, [action.formId, 'stepsCount'], 1); + const formState = state?.[action.formId] || DEFAULT_OBJECT; + const currentStepIndex = state?.[action.formId]?.currentStepIndex || 0; + const stepsCount = state?.[action.formId]?.stepsCount || 1; // Can't go under 0 const newStepIndex = currentStepIndex <= 0 ? 0 : currentStepIndex - 1; @@ -84,9 +89,9 @@ export const reducer = (state = initialState, action: FormAction) => { return state; } - const formState = _.get(state, action.formId, DEFAULT_OBJECT); - const currentStepIndex = _.get(state, [action.formId, 'currentStepIndex'], 0); - const stepsCount = _.get(state, [action.formId, 'stepsCount'], 1); + const formState = state?.[action.formId] || DEFAULT_OBJECT; + const currentStepIndex = state?.[action.formId]?.currentStepIndex || 0; + const stepsCount = state?.[action.formId]?.stepsCount || 1; // Can't go above steps count const newStepIndex = currentStepIndex >= stepsCount - 1 ? stepsCount - 1 : currentStepIndex + 1; @@ -109,7 +114,7 @@ export const reducer = (state = initialState, action: FormAction) => { return state; } - const formState = _.get(state, action.formId); + const formState = state?.[action.formId]; if (!formState) return state; @@ -133,7 +138,9 @@ export const reducer = (state = initialState, action: FormAction) => { return state; } - return _.omit(state, [action.formId]); + const { [action.formId]: targetForm, ...resetState } = state; + + return resetState; } case SET_STEP: { @@ -144,14 +151,14 @@ export const reducer = (state = initialState, action: FormAction) => { return state; } - const newStepIndex = _.isNumber(action.stepIndex) ? action.stepIndex : 0; - const stepsCount = _.get(state, [action.formId, 'stepsCount'], 1); + const newStepIndex = typeof action.stepIndex === 'number' ? action.stepIndex : 0; + const stepsCount = state?.[action.formId]?.stepsCount || 1; if (!checkStepExist(stepsCount, newStepIndex)) { return state; } - const formState = _.get(state, action.formId, DEFAULT_OBJECT); + const formState = state?.[action.formId] || DEFAULT_OBJECT; return { ...state, diff --git a/libs/form-context/src/lib/forms.selectors.ts b/libs/form-context/src/lib/forms.selectors.ts index e53df24..0ea5708 100644 --- a/libs/form-context/src/lib/forms.selectors.ts +++ b/libs/form-context/src/lib/forms.selectors.ts @@ -1,12 +1,18 @@ -import _ from 'lodash'; import { DefaultFormState } from './forms.reducer'; import { FieldValues } from 'react-hook-form'; const defaultData = {} as FieldValues; -export const getFormData = (formId: string) => (state: DefaultFormState) => _.get(state, [formId, 'data'], defaultData); +export const getFormData = (formId: string) => (state: DefaultFormState) => state?.[formId]?.data || defaultData; export const getCurrentStepIndex = (formId: string) => (state: DefaultFormState) => - _.get(state, [formId, 'currentStepIndex'], 0); + state?.[formId].currentStepIndex || 0; -export const isLastStep = (formId: string) => (state: DefaultFormState) => _.get(state, [formId, 'isLastStep'], true); +export const isLastStep = (formId: string) => (state: DefaultFormState) => { + const value = state[formId]?.isLastStep; + if (typeof value === 'boolean') { + return value; + } + + return true; +}; diff --git a/libs/form-editor/package.json b/libs/form-editor/package.json index fb26228..5f8f838 100644 --- a/libs/form-editor/package.json +++ b/libs/form-editor/package.json @@ -1,17 +1,17 @@ { "name": "@bedrockstreaming/form-editor", "version": "0.8.2", - "peerDependencies": { + "dependencies": { "@bedrockstreaming/form-builder": "0.8.1", - "lodash": "4.17.21", - "react": "17.0.2", - "react-hook-form": "7.27.0", - "@hookform/devtools": "4.0.2", - "react-redux": "7.2.6", "@bedrockstreaming/form-redux": "0.8.1", - "@mui/material": "5.2.2", + "@hookform/devtools": "4.0.2", + "classnames": "2.3.1", + "lodash": "4.17.21", "@mui/icons-material": "5.2.0", + "@mui/material": "5.2.2", "@mui/styles": "5.2.2", - "classnames": "2.3.1" + "react": "17.0.2", + "react-hook-form": "7.27.0", + "react-redux": "7.2.6" } } diff --git a/libs/form-redux/package.json b/libs/form-redux/package.json index 34e1a1a..4cab735 100644 --- a/libs/form-redux/package.json +++ b/libs/form-redux/package.json @@ -3,10 +3,9 @@ "version": "0.8.2", "peerDependencies": { "@bedrockstreaming/form-builder": "0.8.1", - "lodash": "4.17.21", + "@hookform/devtools": "4.0.2", "react": "17.0.2", - "react-hook-form": "7.27.0", - "@hookform/devtools": "4.0.2" + "react-hook-form": "7.27.0" }, "devDependencies": { "deep-freeze": "0.0.1" diff --git a/libs/form-redux/src/lib/__tests__/forms.reducer.spec.ts b/libs/form-redux/src/lib/__tests__/forms.reducer.spec.ts index b4d8429..4a363c9 100644 --- a/libs/form-redux/src/lib/__tests__/forms.reducer.spec.ts +++ b/libs/form-redux/src/lib/__tests__/forms.reducer.spec.ts @@ -1,6 +1,7 @@ import deepFreeze from 'deep-freeze'; import { reducer, initialState, DefaultFormState, FormAction } from '../forms.reducer'; import { PREVIOUS_STEP, NEXT_STEP, UPDATE_FORM_DATA, INIT_FORM, RESET_FORM, SET_STEP } from '../forms.actions'; +import { FormSchema } from '@bedrockstreaming/form-builder'; const formId = 'foo'; const emptyObject = {}; @@ -27,7 +28,7 @@ describe('forms.reducer', () => { const action = { type: INIT_FORM, formId, - schema: { stepsById: ['foo'] }, + schema: { stepsById: ['foo'] } as FormSchema, }; expect(freezedReducer(initialState, action)).toEqual(defaultState); }); diff --git a/libs/form-redux/src/lib/__tests__/forms.selectors.spec.ts b/libs/form-redux/src/lib/__tests__/forms.selectors.spec.ts index d8be726..e6a791d 100644 --- a/libs/form-redux/src/lib/__tests__/forms.selectors.spec.ts +++ b/libs/form-redux/src/lib/__tests__/forms.selectors.spec.ts @@ -3,9 +3,9 @@ import { getFormData, getCurrentStepIndex, isLastStep } from '../forms.selectors const formId = 'foo'; const state = { forms: { - foo: { + [formId]: { currentStepIndex: 666, - data: 'bar', + data: { bar: 'baz' }, isLastStep: false, stepsCount: 999, }, @@ -15,19 +15,19 @@ const state = { describe('forms.selectors', () => { describe('getFormData', () => { it('should retrieve data from state input', () => { - expect(getFormData(formId)(state)).toBe(state.forms.foo.data); + expect(getFormData(formId)(state)).toEqual(state.forms.foo.data); }); }); describe('getCurrentStepIndex', () => { it('should retrieve currentStepIndex from state input', () => { - expect(getCurrentStepIndex(formId)(state)).toBe(state.forms.foo.currentStepIndex); + expect(getCurrentStepIndex(formId)(state)).toEqual(state.forms.foo.currentStepIndex); }); }); describe('isLastStep', () => { it('should retrieve isLastStep property', () => { - expect(isLastStep(formId)(state)).toBe(state.forms.foo.isLastStep); + expect(isLastStep(formId)(state)).toEqual(state.forms.foo.isLastStep); }); }); }); diff --git a/libs/form-redux/src/lib/forms.reducer.ts b/libs/form-redux/src/lib/forms.reducer.ts index 1fafce7..c00f553 100644 --- a/libs/form-redux/src/lib/forms.reducer.ts +++ b/libs/form-redux/src/lib/forms.reducer.ts @@ -1,5 +1,5 @@ -import _ from 'lodash'; import { FieldValues } from 'react-hook-form'; +import { FormSchema } from '@bedrockstreaming/form-builder'; import { PREVIOUS_STEP, NEXT_STEP, UPDATE_FORM_DATA, INIT_FORM, RESET_FORM, SET_STEP } from './forms.actions'; @@ -18,6 +18,7 @@ export interface FormAction { data?: { [key: string]: unknown; }; + schema?: FormSchema; [key: string]: unknown; } @@ -28,8 +29,8 @@ const defaultFormState = { data: {}, }; -const DEFAULT_OBJECT = {}; -export const initialState = {} as any; +const DEFAULT_OBJECT = {} as const; +export const initialState = {} as DefaultFormState; const checkFormId = ({ formId }: FormAction) => !!formId; const checkFormExist = ({ formId }: FormAction, state: DefaultFormState) => !!state[formId]; @@ -42,7 +43,7 @@ export const reducer = (state = initialState, action: FormAction) => { return state; } - const stepsById = _.get(action, ['schema', 'stepsById'], [] as string[]); + const stepsById = action?.schema?.stepsById || ([] as string[]); const currentFormState = { ...defaultFormState, stepsCount: stepsById.length, @@ -63,9 +64,9 @@ export const reducer = (state = initialState, action: FormAction) => { return state; } - const formState = _.get(state, action.formId, DEFAULT_OBJECT); - const currentStepIndex = _.get(state, [action.formId, 'currentStepIndex'], 0); - const stepsCount = _.get(state, [action.formId, 'stepsCount'], 1); + const formState = state?.[action.formId] || DEFAULT_OBJECT; + const currentStepIndex = state?.[action.formId]?.currentStepIndex || 0; + const stepsCount = state?.[action.formId]?.stepsCount || 1; // Can't go under 0 const newStepIndex = currentStepIndex <= 0 ? 0 : currentStepIndex - 1; @@ -88,9 +89,9 @@ export const reducer = (state = initialState, action: FormAction) => { return state; } - const formState = _.get(state, action.formId, DEFAULT_OBJECT); - const currentStepIndex = _.get(state, [action.formId, 'currentStepIndex'], 0); - const stepsCount = _.get(state, [action.formId, 'stepsCount'], 1); + const formState = state?.[action.formId] || DEFAULT_OBJECT; + const currentStepIndex = state?.[action.formId]?.currentStepIndex || 0; + const stepsCount = state?.[action.formId]?.stepsCount || 1; // Can't go above steps count const newStepIndex = currentStepIndex >= stepsCount - 1 ? stepsCount - 1 : currentStepIndex + 1; @@ -113,7 +114,7 @@ export const reducer = (state = initialState, action: FormAction) => { return state; } - const formState = _.get(state, action.formId); + const formState = state?.[action.formId]; if (!formState) return state; @@ -137,7 +138,9 @@ export const reducer = (state = initialState, action: FormAction) => { return state; } - return _.omit(state, [action.formId]); + const { [action.formId]: targetForm, ...resetState } = state; + + return resetState; } case SET_STEP: { @@ -148,14 +151,14 @@ export const reducer = (state = initialState, action: FormAction) => { return state; } - const newStepIndex = _.isNumber(action.stepIndex) ? action.stepIndex : 0; - const stepsCount = _.get(state, [action.formId, 'stepsCount'], 1); + const newStepIndex = typeof action.stepIndex === 'number' ? action.stepIndex : 0; + const stepsCount = state?.[action.formId]?.stepsCount || 1; if (!checkStepExist(stepsCount, newStepIndex)) { return state; } - const formState = _.get(state, action.formId, DEFAULT_OBJECT); + const formState = state?.[action.formId] || DEFAULT_OBJECT; return { ...state, diff --git a/libs/form-redux/src/lib/forms.selectors.ts b/libs/form-redux/src/lib/forms.selectors.ts index a990a7e..ff49b21 100644 --- a/libs/form-redux/src/lib/forms.selectors.ts +++ b/libs/form-redux/src/lib/forms.selectors.ts @@ -1,18 +1,18 @@ -import _ from 'lodash'; +import { DefaultFormState } from './forms.reducer'; -const defaultObject = {} as any; +const defaultObject = {} as const; -export const getFormData = - (formId: string) => - (state: RootState) => - _.get(state, ['forms', formId, 'data'], defaultObject); +export const getFormData = (formId: string) => (state: { forms: DefaultFormState }) => + state.forms?.[formId]?.data || defaultObject; -export const getCurrentStepIndex = - (formId: string) => - (state: RootState) => - _.get(state, ['forms', formId, 'currentStepIndex'], 0); +export const getCurrentStepIndex = (formId: string) => (state: { forms: DefaultFormState }) => + state.forms?.[formId]?.currentStepIndex || 0; -export const isLastStep = - (formId: string) => - (state: RootState) => - _.get(state, ['forms', formId, 'isLastStep'], true); +export const isLastStep = (formId: string) => (state: { forms: DefaultFormState }) => { + const value = state.forms[formId]?.isLastStep; + if (typeof value === 'boolean') { + return value; + } + + return true; +}; diff --git a/libs/form-validation-rule-list/package.json b/libs/form-validation-rule-list/package.json index 6263311..59b4d56 100644 --- a/libs/form-validation-rule-list/package.json +++ b/libs/form-validation-rule-list/package.json @@ -5,7 +5,6 @@ "peerDependencies": { "react": "17.0.2", "@bedrockstreaming/form-builder": "0.8.1", - "lodash": "4.17.21", "react-hook-form": "7.27.0", "@hookform/devtools": "4.0.2" } diff --git a/libs/form-validation-rule-list/src/lib/components/validationRuleList.component.tsx b/libs/form-validation-rule-list/src/lib/components/validationRuleList.component.tsx index 3a6b0e3..bbc98dc 100644 --- a/libs/form-validation-rule-list/src/lib/components/validationRuleList.component.tsx +++ b/libs/form-validation-rule-list/src/lib/components/validationRuleList.component.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; -import _ from 'lodash'; import { INCOMPLETE_STATE, STATUS_BY_STATE } from '../constants'; import { RuleObject } from '../rule'; +const noop = () => null; + export interface RulesItem { key: string; status: string; @@ -21,11 +22,14 @@ const getItemsAndErrors = (rules: RuleObject[], value?: string | number) => (acc, { check, key }) => { const result = check(value); - const nextErrors = result === INCOMPLETE_STATE ? _.concat(acc.errors, key) : acc.errors; - const nextItems = _.concat(acc.items, { - key, - status: STATUS_BY_STATE[result], - }); + const nextErrors = result === INCOMPLETE_STATE ? [...acc.errors, key] : acc.errors; + const nextItems = [ + ...acc.items, + { + key, + status: STATUS_BY_STATE[result], + }, + ]; return { items: nextItems, errors: nextErrors }; }, @@ -51,7 +55,7 @@ export const ValidationRuleList = ({ value = '', component: Component, componentProp = 'items', - onError = _.noop, + onError = noop, ...otherProps }: ValidationRuleListProps) => { if (!Component || !rules.length) { diff --git a/libs/form-validation-rule-list/src/lib/getValidationRulesHints.ts b/libs/form-validation-rule-list/src/lib/getValidationRulesHints.ts index 99c4579..68e2d2b 100644 --- a/libs/form-validation-rule-list/src/lib/getValidationRulesHints.ts +++ b/libs/form-validation-rule-list/src/lib/getValidationRulesHints.ts @@ -1,6 +1,5 @@ import { Validations, DEFAULT_RULES_NAMES } from '@bedrockstreaming/form-builder'; import { FieldErrors } from 'react-hook-form'; -import _ from 'lodash'; import { rule, RuleObject } from './rule'; @@ -15,13 +14,10 @@ export interface GetValidationRulesHintsArgs { config?: AbstractMapOfString; } -export const getValidationRulesHints = ({ - t = _.identity, - errors, - validation, - config, -}: GetValidationRulesHintsArgs) => { +const identity = (value: string) => value; + +export const getValidationRulesHints = ({ t = identity, errors, validation, config }: GetValidationRulesHintsArgs) => { return Object.values(validation).reduce((acc, { message, key }) => { - return DEFAULT_RULES_NAMES[key] ? acc : [...acc, rule(t(message, config), () => !_.get(errors, ['types', key]))]; + return DEFAULT_RULES_NAMES[key] ? acc : [...acc, rule(t(message, config), () => !errors?.types[key])]; }, [] as Array); }; diff --git a/libs/form-validation-rule-list/src/lib/rule.ts b/libs/form-validation-rule-list/src/lib/rule.ts index a99995a..e5c51af 100644 --- a/libs/form-validation-rule-list/src/lib/rule.ts +++ b/libs/form-validation-rule-list/src/lib/rule.ts @@ -1,4 +1,3 @@ -import _ from 'lodash'; import { INCOMPLETE_STATE, DEFAULT_STATE, COMPLETE_STATE } from './constants'; export type RuleCheck = (value?: string | number) => boolean; @@ -15,7 +14,7 @@ export type Rule = (key: string, check: RuleCheck) => RuleObject; export const rule = (key: string, check?: RuleCheck): RuleObject => ({ key, check: (value?: string | number) => { - if (typeof value === 'undefined' || !_.get(value, ['length'])) { + if (typeof value === 'undefined' || (typeof value === 'string' && !value?.length)) { return DEFAULT_STATE; } @@ -30,4 +29,4 @@ export const rule = (key: string, check?: RuleCheck): RuleObject => ({ export type CheckRules = (value: string | number, rules: RuleObject[]) => string[]; export const checkRules = (value: string | number | undefined, rules: RuleObject[]): string[] => - rules.reduce((acc, { check, key }) => (check(value) === INCOMPLETE_STATE ? _.concat(acc, key) : acc), [] as string[]); + rules.reduce((acc, { check, key }) => (check(value) === INCOMPLETE_STATE ? [...acc, key] : acc), [] as string[]);