Skip to content

Commit

Permalink
feat: added validate on blur
Browse files Browse the repository at this point in the history
  • Loading branch information
chesterkmr committed Jan 2, 2025
1 parent 2d11cb0 commit 14f1dfd
Show file tree
Hide file tree
Showing 12 changed files with 189 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FunctionComponent, useMemo } from 'react';
import { Renderer, TRendererSchema } from '../../Renderer';
import { ValidatorProvider } from '../Validator';
import { DynamicFormContext, IDynamicFormContext } from './context';
import { defaultValidationParams } from './defaults';
import { useSubmit } from './hooks/external/useSubmit';
import { useFieldHelpers } from './hooks/internal/useFieldHelpers';
import { useTouched } from './hooks/internal/useTouched';
Expand All @@ -15,7 +16,7 @@ import { IDynamicFormProps } from './types';
export const DynamicFormV2: FunctionComponent<IDynamicFormProps> = ({
elements,
values: initialValues,
validationParams,
validationParams = defaultValidationParams,
fieldExtends,
metadata,
onChange,
Expand Down Expand Up @@ -44,8 +45,18 @@ export const DynamicFormV2: FunctionComponent<IDynamicFormProps> = ({
onEvent,
},
metadata: metadata ?? {},
validationParams: validationParams ?? {},
}),
[touchedApi.touched, valuesApi.values, submit, fieldHelpers, fieldExtends, onEvent, metadata],
[
touchedApi.touched,
valuesApi.values,
submit,
fieldHelpers,
fieldExtends,
onEvent,
metadata,
validationParams,
],
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,32 @@ describe('DynamicFormV2', () => {
onEvent: mockProps.onEvent,
},
metadata: mockProps.metadata,
validationParams: mockProps.validationParams,
});
});

it('should use default validation params when not provided in props', () => {
const propsWithoutValidation = { ...mockProps };
delete propsWithoutValidation.validationParams;

render(<DynamicFormV2 {...propsWithoutValidation} />);

const providerProps = vi.mocked(DynamicFormContext.Provider).mock.calls[0]?.[0];

expect(providerProps?.value.validationParams).toEqual({
validateOnBlur: true,
});
});

it('should use validation params from props when provided', () => {
const customValidationParams = {
validateOnBlur: false,
};

render(<DynamicFormV2 {...mockProps} validationParams={customValidationParams} />);

const providerProps = vi.mocked(DynamicFormContext.Provider).mock.calls[0]?.[0];

expect(providerProps?.value.validationParams).toEqual(customValidationParams);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ import { AnyObject } from '@/common';
import { useState } from 'react';
import { JSONEditorComponent } from '../../../Validator/_stories/components/JsonEditor/JsonEditor';
import { DynamicFormV2 } from '../../DynamicForm';
import { IDynamicFormValidationParams } from '../../types';
import { schema } from './schema';

const validationParams: IDynamicFormValidationParams = {
validateOnBlur: false,
};

export const ValidationShowcaseComponent = () => {
const [context, setContext] = useState<AnyObject>({});

Expand All @@ -17,6 +22,7 @@ export const ValidationShowcaseComponent = () => {
console.log('onSubmit');
}}
onChange={setContext}
validationParams={validationParams}
// onEvent={console.log}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IFormEventElement, TElementEvent } from '../hooks/internal/useEvents/types';
import { IFieldHelpers } from '../hooks/internal/useFieldHelpers/types';
import { ITouchedState } from '../hooks/internal/useTouched';
import { TElementsMap } from '../types';
import { IDynamicFormValidationParams, TElementsMap } from '../types';

export interface IDynamicFormCallbacks {
onEvent?: (eventName: TElementEvent, element: IFormEventElement<any, any>) => void;
Expand All @@ -15,4 +15,5 @@ export interface IDynamicFormContext<TValues extends object> {
submit: () => void;
callbacks: IDynamicFormCallbacks;
metadata: Record<string, string>;
validationParams: IDynamicFormValidationParams;
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ describe('SubmitButton', () => {
onFocus: vi.fn(),
});
vi.mocked(useDynamicForm).mockReturnValue({
validationParams: { validateOnBlur: false },
fieldHelpers: mockFieldHelpers,
submit: vi.fn(),
values: {},
Expand Down Expand Up @@ -112,6 +113,7 @@ describe('SubmitButton', () => {
const mockRunTasks = vi.fn();

vi.mocked(useDynamicForm).mockReturnValue({
validationParams: { validateOnBlur: false },
fieldHelpers: mockFieldHelpers,
submit: mockSubmit,
values: {},
Expand Down Expand Up @@ -148,6 +150,7 @@ describe('SubmitButton', () => {
const mockRunTasks = vi.fn();

vi.mocked(useDynamicForm).mockReturnValue({
validationParams: { validateOnBlur: false },
fieldHelpers: mockFieldHelpers,
submit: mockSubmit,
values: {},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { IDynamicFormValidationParams } from './types';

export const defaultValidationParams: IDynamicFormValidationParams = {
validateOnBlur: true,
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,29 @@ import { IFormElement } from '../../types';

export const convertFormElementsToValidationSchema = (
elements: Array<IFormElement<any>>,
schema: IValidationSchema[] = [],
): IValidationSchema[] => {
return elements.map(element => {
const validationSchema: IValidationSchema = {
id: element.id,
valueDestination: element.valueDestination,
validators: element.validate || [],
};
const filteredElements = elements.filter(
element => element.valueDestination || element.children?.length,
);

if (element.children) {
validationSchema.children = convertFormElementsToValidationSchema(element.children);
for (let i = 0; i < filteredElements.length; i++) {
const element = filteredElements[i]!;

if (element.valueDestination) {
schema.push({
id: element.id,
valueDestination: element.valueDestination,
validators: element.validate || [],
});

if (element.children?.length) {
schema[i]!.children = convertFormElementsToValidationSchema(element.children || []);
}
} else {
convertFormElementsToValidationSchema(element.children || [], schema);
}
}

return validationSchema;
});
return schema;
};
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,70 @@ describe('convertFormElementsToValidationSchema', () => {
],
] as const;

const cases = [case1, case2];
const case3 = [
[
{
id: 'somenestedfield',
children: [
{
id: 'field',
valueDestination: 'test',
validate: [{ type: 'required' }],
},
{
id: 'nestedmore',
children: [
{
id: 'nestedmore2',
valueDestination: 'test',
validate: [{ type: 'required' }],
},
],
},
{
id: 'level1',
children: [
{
id: 'level2',
children: [
{
id: 'level3',
children: [
{
id: 'level4',
children: [
{
id: 'level5',
valueDestination: 'test',
validate: [{ type: 'required' }],
},
],
},
],
},
],
},
],
},
],
},
] as IFormElement[],
[
{ id: 'field', valueDestination: 'test', validators: [{ type: 'required' }] },
{
id: 'nestedmore2',
valueDestination: 'test',
validators: [{ type: 'required' }],
},
{
id: 'level5',
valueDestination: 'test',
validators: [{ type: 'required' }],
},
] as const,
] as const;

const cases = [case1, case2, case3];

test.each(cases)('should convert form elements to validation schema', (schema, output) => {
const validationSchema = convertFormElementsToValidationSchema(schema);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useRuleEngine } from '@/components/organisms/Form/hooks';
import { TDeepthLevelStack } from '@/components/organisms/Form/Validator';
import { TDeepthLevelStack, useValidator } from '@/components/organisms/Form/Validator';
import { useCallback, useMemo } from 'react';
import { useDynamicForm } from '../../../context';
import { IFormElement } from '../../../types';
Expand All @@ -14,8 +14,9 @@ export const useField = <TValue>(
const fieldId = useElementId(element, stack);
const valueDestination = useValueDestination(element, stack);

const { fieldHelpers, values } = useDynamicForm();
const { fieldHelpers, values, validationParams } = useDynamicForm();
const { sendEvent, sendEventAsync } = useEvents(element);
const { validate } = useValidator();
const { setValue, getValue, setTouched, getTouched } = fieldHelpers;

const value = useMemo(() => getValue<TValue>(valueDestination), [valueDestination, getValue]);
Expand Down Expand Up @@ -47,7 +48,12 @@ export const useField = <TValue>(

const onBlur = useCallback(() => {
sendEvent('onBlur');
}, [sendEvent]);
setTouched(fieldId, true);

if (validationParams.validateOnBlur) {
validate();
}
}, [sendEvent, validationParams.validateOnBlur, validate, fieldId, setTouched]);

const onFocus = useCallback(() => {
sendEvent('onFocus');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IRuleExecutionResult, useRuleEngine } from '@/components/organisms/Form/hooks';
import { renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useValidator } from '../../../../Validator';
import { IDynamicFormContext, useDynamicForm } from '../../../context';
import { ICommonFieldParams, IFormElement } from '../../../types';
import { useEvents } from '../../internal/useEvents';
Expand Down Expand Up @@ -29,6 +30,10 @@ vi.mock('../../internal/useEvents', () => ({
useEvents: vi.fn(),
}));

vi.mock('../../../../Validator', () => ({
useValidator: vi.fn(),
}));

describe('useField', () => {
const mockElement = {
id: 'test-field',
Expand All @@ -45,6 +50,7 @@ describe('useField', () => {
const mockGetTouched = vi.fn();
const mockSendEvent = vi.fn();
const mockSendEventAsync = vi.fn();
const mockValidate = vi.fn();

const mockFieldHelpers = {
setValue: mockSetValue,
Expand All @@ -66,7 +72,13 @@ describe('useField', () => {
vi.mocked(useDynamicForm).mockReturnValue({
fieldHelpers: mockFieldHelpers,
values: {},
validationParams: {
validateOnBlur: true,
},
} as unknown as IDynamicFormContext<object>);
vi.mocked(useValidator).mockReturnValue({
validate: mockValidate,
} as any);
mockGetValue.mockReturnValue('test-value');
mockGetTouched.mockReturnValue(false);

Expand Down Expand Up @@ -137,12 +149,30 @@ describe('useField', () => {
});

describe('onBlur', () => {
it('should trigger blur event', () => {
it('should trigger blur event and validate when validateOnBlur is true', () => {
const { result } = renderHook(() => useField(mockElement, mockStack));

result.current.onBlur();

expect(mockSendEvent).toHaveBeenCalledWith('onBlur');
expect(mockValidate).toHaveBeenCalled();
});

it('should not validate when validateOnBlur is false', () => {
vi.mocked(useDynamicForm).mockReturnValue({
fieldHelpers: mockFieldHelpers,
values: {},
validationParams: {
validateOnBlur: false,
},
} as unknown as IDynamicFormContext<object>);

const { result } = renderHook(() => useField(mockElement, mockStack));

result.current.onBlur();

expect(mockSendEvent).toHaveBeenCalledWith('onBlur');
expect(mockValidate).not.toHaveBeenCalled();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { convertFormElementsToValidationSchema } from '../../../helpers/convert-
import { IFormElement } from '../../../types';

export const useValidationSchema = (elements: Array<IFormElement<any, any>>) => {
const validationSchema = useMemo(() => {
return convertFormElementsToValidationSchema(elements);
}, [elements]);
const validationSchema = useMemo(
() => convertFormElementsToValidationSchema(elements),
[elements],
);

return validationSchema;
};
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,17 @@ export type TDynamicFormField<TParams = object> = FunctionComponent<{

export type TElementsMap = Record<string, TDynamicFormElement<any, any>>;

export interface IDynamicFormValidationParams extends IValidationParams {
validateOnBlur?: boolean;
}

export interface IDynamicFormProps<TValues = object> {
values: TValues;
elements: Array<IFormElement<string, any>>;

fieldExtends?: Record<string, TDynamicFormField<any> | TDynamicFormElement<any, any>>;

validationParams?: IValidationParams;
validationParams?: IDynamicFormValidationParams;
onChange?: (newValues: TValues) => void;
onFieldChange?: (fieldName: string, newValue: unknown, newValues: TValues) => void;
onSubmit?: (values: TValues) => void;
Expand Down

0 comments on commit 14f1dfd

Please sign in to comment.