diff --git a/README.md b/README.md index 04ce25d..8b29ee5 100644 --- a/README.md +++ b/README.md @@ -3,37 +3,25 @@ Allows you to easily validate [Formik](https://github.com/jaredpalmer/formik) forms with the power of [Zod](https://github.com/colinhacks/zod) schemas. -WARNING: As of v2, this package uses native ESM and no longer provides a -CommonJS export. If this is something you need, you should be able to use the -[dynamic import](https://v8.dev/features/dynamic-import) function or use v1 of -this package. +> [!IMPORTANT] WARNING: As of v2.0, this package uses ESM and no longer provides +> a CommonJS export. If this is something you need, you should be able to use +> the [dynamic import](https://v8.dev/features/dynamic-import) function. ## Installation -This package is published both on [NPM](https://www.npmjs.com) and -[JSR](https://jsr.io/). - To install from NPM: ```sh npm install formik-validator-zod +pnpm add formik-validator-zod + yarn add formik-validator-zod bun add formik-validator-zod ``` -To install from JSR: - -```sh -npx jsr add @glazy/formik-validator-zod - -yarn dlx jsr add @glazy/formik-validator-zod - -bunx jsr add @glazy/formik-validator-zod -``` - -## Example +## Usage ```jsx import { Formik } from 'formik' @@ -54,3 +42,12 @@ const MyForm = () => { ) } ``` + +## Is this library still maintained? + +Yes! This library is used in a couple of production codebases that I'm aware of, +including my current employers. + +I don't expect the library will need a lot of active maintenance going forwards. +This is due to its limited scope and the fact Formik itsely seems to be +abandoned. diff --git a/lib/index.ts b/lib/index.ts index 95076ed..0cdf510 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,4 +1,5 @@ import type { ZodSchema, ParseParams } from 'zod' +import merge from 'deepmerge' /** * Allows you to easily use Zod schemas with the component `validate` @@ -16,7 +17,21 @@ export const withZodSchema = if (result.success) return {} return result.error.issues.reduce((acc, curr) => { - const key = curr.path.join('.') + if (curr.path.length) { + return merge( + acc, + curr.path.reduceRight( + (errors, pathSegment) => ({ + [pathSegment]: !Object.keys(errors).length + ? curr.message + : errors, + }), + {} + ) + ) + } + + const key = curr.path[0] return { ...acc, [key]: curr.message, diff --git a/package-lock.json b/package-lock.json index 1bdad5f..f4ffe26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,16 @@ { "name": "formik-validator-zod", - "version": "2.0.0", + "version": "2.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "formik-validator-zod", - "version": "2.0.0", + "version": "2.0.1", "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1" + }, "devDependencies": { "formik": "^2.2.9", "husky": "^8.0.0", @@ -267,10 +270,9 @@ } }, "node_modules/deepmerge": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", - "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", - "dev": true, + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "engines": { "node": ">=0.10.0" } @@ -701,6 +703,15 @@ "react": ">=16.8.0" } }, + "node_modules/formik/node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -1656,10 +1667,9 @@ } }, "deepmerge": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", - "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", - "dev": true + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" }, "end-of-stream": { "version": "1.4.4", @@ -1880,6 +1890,14 @@ "react-fast-compare": "^2.0.1", "tiny-warning": "^1.0.2", "tslib": "^1.10.0" + }, + "dependencies": { + "deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "dev": true + } } }, "fsevents": { diff --git a/package.json b/package.json index ae1c14d..a26b5e1 100644 --- a/package.json +++ b/package.json @@ -34,5 +34,8 @@ "peerDependencies": { "formik": "^2.2.9", "zod": "^3.19.1" + }, + "dependencies": { + "deepmerge": "^4.3.1" } } diff --git a/test/index.test.ts b/test/index.test.ts index d61beaf..70bed52 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import { z } from 'zod' +import { getIn } from 'formik' import { withZodSchema } from '../lib/index' const testSchema = z.object({ @@ -36,4 +37,135 @@ describe('withZodSchema', () => { favouriteValue: 'Invalid input', }) }) + + describe('nested objects', () => { + const schema = z.object({ + user: z.object({ + name: z.string().min(2), + email: z.string().email(), + }), + }) + + it('returns no errors for valid data', () => { + const result = withZodSchema(schema)({ + user: { + name: 'Luke', + email: 'Glazy@users.noreply.github.com', + }, + }) + + expect(Object.keys(result).length).toEqual(0) + }) + + it('returns field error if no children have errors', () => { + // @ts-ignore Type incorrect for usage in test case. + const result = withZodSchema(schema)({}) + + expect(getIn(result, 'user')).toEqual('Required') + }) + + it('returns object errors correctly', () => { + const result = withZodSchema(schema)({ + user: { + name: 'X', + email: 'invalid-email', + }, + }) + + expect(getIn(result, 'user.name')).toEqual( + 'String must contain at least 2 character(s)' + ) + expect(getIn(result, 'user.email')).toEqual('Invalid email') + }) + }) + + describe('simple arrays', () => { + const schema = z.object({ + favouriteColours: z.string().array().min(3), + }) + + it('returns field issue if children are valid', () => { + const result = withZodSchema(schema)({ + favouriteColours: ['Yellow', 'Red'], + }) + + expect(getIn(result, 'favouriteColours')).toEqual( + 'Array must contain at least 3 element(s)' + ) + }) + + it('returns no errors for valid data', () => { + const result = withZodSchema(schema)({ + favouriteColours: ['Yellow', 'Blue', 'Purple', 'Red'], + }) + + expect(Object.keys(result).length).toEqual(0) + }) + + it('returns error in array correctly', () => { + const result = withZodSchema(schema)({ + // @ts-ignore Incorrect type to facilitate test case. + favouriteColours: ['Yellow', 'Red', 42], + }) + + expect(getIn(result, 'favouriteColours.1')).toBeUndefined() + expect(getIn(result, 'favouriteColours.2')).toEqual( + 'Expected string, received number' + ) + }) + }) + + describe('array of objects', () => { + const schema = z.object({ + footballTeams: z + .object({ + name: z.string().endsWith('FC'), + manager: z.string().min(2), + }) + .array() + .min(2), + }) + + it('returns no errors for correct data', () => { + const result = withZodSchema(schema)({ + footballTeams: [ + { name: 'Green FC', manager: 'Mr Green' }, + { name: 'Red FC', manager: 'Mr Red' }, + ], + }) + + expect(Object.keys(result).length).toEqual(0) + }) + + it('returns field error if no children have errors', () => { + const result = withZodSchema(schema)({ + footballTeams: [{ name: 'Green FC', manager: 'Mr Green' }], + }) + + expect(getIn(result, 'footballTeams')).toEqual( + 'Array must contain at least 2 element(s)' + ) + }) + + it('returns errors correctly', () => { + const result = withZodSchema(schema)({ + footballTeams: [ + { name: 'Green Athletic', manager: 'X' }, + { name: 'Red FC', manager: 'X' }, + ], + }) + + expect(getIn(result, 'footballTeams[0].name')).toEqual( + 'Invalid input: must end with "FC"' + ) + expect(getIn(result, 'footballTeams[0].manager')).toEqual( + 'String must contain at least 2 character(s)' + ) + + expect(getIn(result, 'footballTeams[1].name')).toBeUndefined() + expect(getIn(result, 'footballTeams[1].manager')).toEqual( + 'String must contain at least 2 character(s)' + ) + }) + }) })