Skip to content

Commit

Permalink
refactor(libs/form-builder): remove lodash usage (#59)
Browse files Browse the repository at this point in the history
  • Loading branch information
hpierre74 authored Feb 23, 2022
1 parent e809860 commit f27205b
Show file tree
Hide file tree
Showing 25 changed files with 186 additions and 174 deletions.
1 change: 0 additions & 1 deletion libs/form-builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"react-hook-form": "7.27.0"
},
"peerDependencies": {
"lodash": "4.17.21",
"react": "17.0.2"
},
"devDependencies": {
Expand Down
3 changes: 1 addition & 2 deletions libs/form-builder/src/lib/__tests__/fixtures.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => ({
Expand Down Expand Up @@ -69,4 +68,4 @@ export const CORRECT_DICTIONARY: Dictionary = {
),
};

export const typesAllowed = _.keys(CORRECT_DICTIONARY);
export const typesAllowed = Object.keys(CORRECT_DICTIONARY);
5 changes: 2 additions & 3 deletions libs/form-builder/src/lib/components/stepper.component.tsx
Original file line number Diff line number Diff line change
@@ -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}</>;
Expand Down
69 changes: 34 additions & 35 deletions libs/form-builder/src/lib/formBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -48,7 +48,7 @@ export function FormBuilder({
schema,
dictionary,
onSubmit,
onNextStep = _.noop,
onNextStep = NOOP,
extraValidation,
defaultValues,
behavior = 'onChange',
Expand All @@ -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),
Expand All @@ -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],
);

Expand All @@ -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 (
<>
Expand All @@ -138,9 +134,9 @@ export function FormBuilder({
onSubmit={handleSubmit(onSubmit)}
>
<Stepper currentStepIndex={currentStepIndex}>
{_.map(stepsById, (stepId) => (
{stepsById?.map((stepId) => (
<React.Fragment key={stepId}>
{_.map(filteredFields, (fieldId) => {
{filteredFields?.map((fieldId) => {
const { type, id, defaultValue, meta, validation } = fields[fieldId];

return (
Expand All @@ -150,21 +146,24 @@ export function FormBuilder({
control={control}
defaultValue={defaultValue}
rules={validationRulesById[fieldId]}
render={({ field }) => (
<FormField
id={id}
fieldType={type}
validation={validation}
dictionary={dictionary}
errors={_.get(errors, [id])}
setFieldValue={setFieldValue}
triggerValidationField={triggerValidationField}
{..._.omit(field, 'ref')}
propRef={field.ref}
isValidating={isValidating}
{...meta}
/>
)}
render={({ field }) => {
const { ref, ...fieldRest } = field;
return (
<FormField
id={id}
fieldType={type}
validation={validation}
dictionary={dictionary}
errors={errors?.[id]}
setFieldValue={setFieldValue}
triggerValidationField={triggerValidationField}
propRef={ref}
isValidating={isValidating}
{...meta}
{...fieldRest}
/>
);
}}
/>
);
})}
Expand Down
5 changes: 2 additions & 3 deletions libs/form-builder/src/lib/hooks/useAutoFocus.hook.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useEffect } from 'react';
import _ from 'lodash';
import { FieldValues, UseFormSetFocus } from 'react-hook-form';
import { FormSchema } from '../types';

Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion libs/form-builder/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,5 @@ export interface Dictionary {
}

export interface ExtraValidation {
[key: string]: (value?: any) => (input?: any) => boolean | string | Promise<boolean | string>;
[key: string]: (value?: any) => (input?: any) => boolean | string | undefined | Promise<boolean | string | undefined>;
}
9 changes: 4 additions & 5 deletions libs/form-builder/src/lib/utils/error.util.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
};
Expand All @@ -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,
Expand All @@ -32,7 +31,7 @@ export const isFieldNotDirtyAndEmpty = ({
fieldToCheck: string;
dirtyFields: DirtyFields;
defaultValues?: DefaultValues<FieldValues>;
}) => !_.get(dirtyFields, fieldToCheck) && !_.get(defaultValues, fieldToCheck);
}) => !dirtyFields?.[fieldToCheck] && !defaultValues?.[fieldToCheck];

export const isStepInError = ({
fieldsToCheckByStep,
Expand Down
21 changes: 10 additions & 11 deletions libs/form-builder/src/lib/utils/getSchemaInfo.util.ts
Original file line number Diff line number Diff line change
@@ -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;
});
Expand All @@ -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,
Expand Down
19 changes: 11 additions & 8 deletions libs/form-builder/src/lib/utils/handleFormBuilderError.util.ts
Original file line number Diff line number Diff line change
@@ -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,
);

Expand All @@ -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,
)}`,
Expand Down
1 change: 1 addition & 0 deletions libs/form-builder/src/lib/utils/object.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isEmpty = (obj: unknown) => !obj || typeof obj !== 'object' || Object.keys(obj || {}).length === 0;
30 changes: 16 additions & 14 deletions libs/form-builder/src/lib/utils/validation.utils.ts
Original file line number Diff line number Diff line change
@@ -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<boolean | string | undefined>, message: string) =>
async (input: any): Promise<string | boolean | undefined> => {
const result = await validate(input);

return result || message;
};
Expand All @@ -21,23 +22,24 @@ export interface FieldRules extends RegisterOptions {
validate?: { [key: string]: (value?: any) => Promise<boolean> | 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;

Expand Down
5 changes: 2 additions & 3 deletions libs/form-context/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions libs/form-context/src/lib/__tests__/forms.selectors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
Loading

0 comments on commit f27205b

Please sign in to comment.