Skip to content

Commit

Permalink
feat: add support for root errors for field array
Browse files Browse the repository at this point in the history
  • Loading branch information
jorisre committed Aug 20, 2023
1 parent a9d319d commit 1f3d596
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 46 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"typescript",
"typescriptreact"
],
"prettier.configPath": "./.prettierrc.js",
"prettier.configPath": "./prettier.config.cjs",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
Expand Down
File renamed without changes.
206 changes: 206 additions & 0 deletions src/__tests__/toNestError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { Field, FieldError, InternalFieldName } from 'react-hook-form';
import { toNestError } from '../toNestError';

const flatObject: Record<string, FieldError> = {
name: { type: 'st', message: 'first message' },
'test.0.name': { type: 'nd', message: 'second message' },
};

const fields = {
name: {
ref: {
reportValidity: vi.fn(),
setCustomValidity: vi.fn(),
},
},
unused: {
ref: { name: 'unusedRef' },
},
} as any as Record<InternalFieldName, Field['_f']>;

test('transforms flat object to nested object', () => {
expect(
toNestError(flatObject, { fields, shouldUseNativeValidation: false }),
).toMatchSnapshot();
});

test('transforms flat object to nested object and shouldUseNativeValidation: true', () => {
expect(
toNestError(flatObject, { fields, shouldUseNativeValidation: true }),
).toMatchSnapshot();
expect(
(fields.name.ref as HTMLInputElement).reportValidity,
).toHaveBeenCalledTimes(1);
expect(
(fields.name.ref as HTMLInputElement).setCustomValidity,
).toHaveBeenCalledTimes(1);
expect(
(fields.name.ref as HTMLInputElement).setCustomValidity,
).toHaveBeenCalledWith(flatObject.name.message);
});

test('transforms flat object to nested object with root error for field array', () => {
const result = toNestError(
{
username: { type: 'username', message: 'username is required' },
'fieldArrayWithRootError.0.name': {
type: 'first',
message: 'first message',
},
'fieldArrayWithRootError.0.nestFieldArrayWithoutRootError.0.title': {
type: 'title',
message: 'title',
},
'fieldArrayWithRootError.0.nestFieldArrayWithRootError': {
type: 'nested-root-title',
message: 'nested root errors',
},
'fieldArrayWithRootError.0.nestFieldArrayWithRootError.0.title': {
type: 'nestFieldArrayWithRootError-title',
message: 'nestFieldArrayWithRootError-title',
},
'fieldArrayWithRootError.1.name': {
type: 'second',
message: 'second message',
},
fieldArrayWithRootError: { type: 'root-error', message: 'root message' },
'fieldArrayWithoutRootError.0.name': {
type: 'first',
message: 'first message',
},
'fieldArrayWithoutRootError.1.name': {
type: 'second',
message: 'second message',
},
},
{
fields: {
username: { name: 'username', ref: { name: 'username' } },
fieldArrayWithRootError: {
name: 'fieldArrayWithRootError',
ref: { name: 'fieldArrayWithRootError' },
},
'fieldArrayWithRootError.0.name': {
name: 'fieldArrayWithRootError.0.name',
ref: { name: 'fieldArrayWithRootError.0.name' },
},
'fieldArrayWithRootError.0.nestFieldArrayWithoutRootError.0.title': {
name: 'fieldArrayWithRootError.0.nestFieldArrayWithoutRootError.0.title',
ref: {
name: 'fieldArrayWithRootError.0.nestFieldArrayWithoutRootError.0.title',
},
},
'fieldArrayWithRootError.0.nestFieldArrayWithRootError': {
name: 'fieldArrayWithRootError.0.nestFieldArrayWithRootError',
ref: {
name: 'fieldArrayWithRootError.0.nestFieldArrayWithRootError',
},
},
'fieldArrayWithRootError.0.nestFieldArrayWithRootError.0.title': {
name: 'fieldArrayWithRootError.0.nestFieldArrayWithRootError.0.title',
ref: {
name: 'fieldArrayWithRootError.0.nestFieldArrayWithRootError.0.title',
},
},
'fieldArrayWithRootError.1.name': {
name: 'fieldArrayWithRootError.1.name',
ref: { name: 'fieldArrayWithRootError.1.name' },
},
'fieldArrayWithoutRootError.0.name': {
name: 'fieldArrayWithoutRootError.0.name',
ref: { name: 'fieldArrayWithoutRootError.0.name' },
},
'fieldArrayWithoutRootError.1.name': {
name: 'fieldArrayWithoutRootError.1.name',
ref: { name: 'fieldArrayWithoutRootError.1.name' },
},
},
names: [
'username',
'fieldArrayWithRootError',
'fieldArrayWithRootError.0.name',
'fieldArrayWithRootError.0.nestFieldArrayWithoutRootError.0.title',
'fieldArrayWithRootError.1.name',
'fieldArrayWithoutRootError.0.name',
'fieldArrayWithoutRootError.1.name',
'fieldArrayWithRootError.0.nestFieldArrayWithRootError',
'fieldArrayWithRootError.0.nestFieldArrayWithRootError.0.title',
],
shouldUseNativeValidation: false,
},
);

expect(result).toEqual({
username: {
type: 'username',
message: 'username is required',
ref: { name: 'username' },
},
fieldArrayWithRootError: {
'0': {
name: {
type: 'first',
message: 'first message',
ref: { name: 'fieldArrayWithRootError.0.name' },
},
nestFieldArrayWithoutRootError: [
{
title: {
type: 'title',
message: 'title',
ref: {
name: 'fieldArrayWithRootError.0.nestFieldArrayWithoutRootError.0.title',
},
},
},
],
nestFieldArrayWithRootError: {
'0': {
title: {
type: 'nestFieldArrayWithRootError-title',
message: 'nestFieldArrayWithRootError-title',
ref: {
name: 'fieldArrayWithRootError.0.nestFieldArrayWithRootError.0.title',
},
},
},
root: {
type: 'nested-root-title',
message: 'nested root errors',
ref: {
name: 'fieldArrayWithRootError.0.nestFieldArrayWithRootError',
},
},
},
},
'1': {
name: {
type: 'second',
message: 'second message',
ref: { name: 'fieldArrayWithRootError.1.name' },
},
},
root: {
type: 'root-error',
message: 'root message',
ref: { name: 'fieldArrayWithRootError' },
},
},
fieldArrayWithoutRootError: [
{
name: {
type: 'first',
message: 'first message',
ref: { name: 'fieldArrayWithoutRootError.0.name' },
},
},
{
name: {
type: 'second',
message: 'second message',
ref: { name: 'fieldArrayWithoutRootError.1.name' },
},
},
],
});
});
40 changes: 0 additions & 40 deletions src/__tests__/toNestObject.ts

This file was deleted.

35 changes: 30 additions & 5 deletions src/toNestError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Field,
ResolverOptions,
FieldValues,
InternalFieldName,
} from 'react-hook-form';
import { validateFieldsNatively } from './validateFieldsNatively';

Expand All @@ -17,13 +18,37 @@ export const toNestError = <TFieldValues extends FieldValues>(
const fieldErrors = {} as FieldErrors<TFieldValues>;
for (const path in errors) {
const field = get(options.fields, path) as Field['_f'] | undefined;
const error = Object.assign(errors[path] || {}, {
ref: field && field.ref,
});

set(
fieldErrors,
path,
Object.assign(errors[path] || {}, { ref: field && field.ref }),
);
if (isNameInFieldArray(options.names || Object.keys(errors), path)) {
updateFieldArrayRootError(fieldErrors, error, path);
} else {
set(fieldErrors, path, error);
}
}

return fieldErrors;
};

const updateFieldArrayRootError = <T extends FieldValues = FieldValues>(
errors: FieldErrors<T>,
error: unknown,
name: InternalFieldName,
): FieldErrors<T> => {
const fieldArrayErrors = Object.assign({}, compact(get(errors, name)));

set(fieldArrayErrors, 'root', error);
set(errors, name, fieldArrayErrors);

return errors;
};

const compact = <TValue>(value: TValue[]) =>
Array.isArray(value) ? value.filter(Boolean) : [];

const isNameInFieldArray = (
names: InternalFieldName[],
name: InternalFieldName,
) => names.some((n) => n.startsWith(name + '.'));

0 comments on commit 1f3d596

Please sign in to comment.