Skip to content

Commit

Permalink
fix: better form handling (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
sahinvardar authored Jul 5, 2024
1 parent 5dd30f8 commit 6d8631f
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 168 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ module.exports = {
'*.postcss',
'*.html',
'*.md',
'*.css'
'*.css',
'*.svelte'
],
overrides: [],
parserOptions: {
Expand Down
18 changes: 9 additions & 9 deletions examples/svelte/src/lib/components/form.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
setValidationErrorsToForm,
setFormDataToForm,
objectToFormData,
clearFormValidationErrors
} from "@vardario/zod-form-validation";
clearFormValidationErrors
} from '@vardario/zod-form-validation';
</script>

<script lang="ts">
import type { ZodAny, AnyZodObject, z, ZodEffects } from "zod";
import type { PartialDeep } from "type-fest";
import type { ZodAny, AnyZodObject, z, ZodEffects } from 'zod';
import type { PartialDeep } from 'type-fest';
type T = $$Generic<AnyZodObject | ZodAny | ZodEffects<AnyZodObject>>;
type ObjectType = z.infer<typeof schema>;
Expand All @@ -24,19 +24,19 @@
$: {
form && data && setFormDataToForm(form, data);
data && console.log( objectToFormData(data))
data && console.log(objectToFormData(data));
}
$: form && setRequiresToForm(form, schema);
function enhance(_form: HTMLFormElement) {
clearFormValidationErrors(_form)
clearFormValidationErrors(_form);
form = _form;
}
function fromValidation() {
const result = validateFormData(new FormData(form), schema);
if (result.success === false) {
if (result.success === false) {
setValidationErrorsToForm(form, result.error);
return false;
}
Expand All @@ -50,7 +50,7 @@
use:enhance
on:input={() => doValidateOnInput && fromValidation()}
on:submit
on:submit={(event) => {
on:submit={event => {
doValidateOnInput = true;
if (!fromValidation()) {
event.preventDefault();
Expand Down
39 changes: 39 additions & 0 deletions examples/svelte/src/lib/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import z from 'zod';

const petsSchema = z.enum(['dog', 'cat', 'hamster', 'parrot', 'spider', 'goldfish']);
const monsterSchema = z.enum(['kraken', 'sasquatch', 'mothman']);
const fruits = z.enum(['apple', 'banana', 'kiwi']);

export const schema = z.object({
admin: z.boolean().refine(val => val === true, {
message: 'Please read and accept the terms and conditions'
}),
birthday: z.string().optional(),
givenName: z.string().min(1),
surname: z.string().min(1),
contact: z.object({
tel: z.string(),
email: z
.object({
email: z.string().email(),
confirmEmail: z.string().email()
})
.superRefine(({ email, confirmEmail }, context) => {
if (email !== confirmEmail) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: 'E-Mail does not match',
path: ['confirmEmail']
});
}
})
}),
amount: z.bigint(),
range: z.number(),
pet: petsSchema,
fruits: z.array(fruits),
description: z.string(),
monster: z.object({
value: monsterSchema
})
});
39 changes: 1 addition & 38 deletions examples/svelte/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,7 @@
import { z } from 'zod';
import { Input, Select, Form, Textarea, Fieldset } from '$lib/index.js';
import { parseFormData } from '@vardario/zod-form-validation';
import { parse } from 'svelte/compiler';
const petsSchema = z.enum(['dog', 'cat', 'hamster', 'parrot', 'spider', 'goldfish']);
const monsterSchema = z.enum(['kraken', 'sasquatch', 'mothman']);
const fruits = z.enum(['apple', 'banana', 'kiwi']);
const schema = z.object({
admin: z.boolean().optional(),
birthday: z.string().optional(),
givenName: z.string().min(1),
surname: z.string().min(1),
contact: z.object({
tel: z.string(),
email: z
.object({
email: z.string().email(),
confirmEmail: z.string().email(),
})
.superRefine(({ email, confirmEmail }, context) => {
if (email !== confirmEmail) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: 'E-Mail does not match',
path: ['confirmEmail'],
});
}
}),
}),
amount: z.bigint(),
range: z.number(),
pet: petsSchema,
fruits: z.array(fruits),
description: z.string(),
monster: z.object({
value: monsterSchema,
}),
});
import { schema } from '$lib/schema.js';
function submit(event: Event) {
const result = parseFormData(new FormData(event.target! as HTMLFormElement), schema);
Expand Down
17 changes: 11 additions & 6 deletions src/form-validation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { flattenSchema, formDataToObject, groupIssuesByName, objectToFormData, preprocess } from './utils.js';
import { flattenSchema, formDataToObject, groupIssuesByName, objectToFormData } from './utils.js';

export const DATA_VALIDATION_ERROR_ATTRIBUTE_NAME = 'data-validation-error';
export const DATA_VALIDATION_ERROR_MESSAGE_ATTRIBUTE_NAME = 'data-validation-error-message';
Expand All @@ -23,11 +23,17 @@ function setSelectElementValue(selectElement: HTMLSelectElement, values: string[

function setInputElementValue(inputElement: HTMLInputElement, value: string) {
if (inputElement.type === 'checkbox' || inputElement.type === 'radio') {
inputElement.checked = inputElement.value === value.replace('true', 'on');
inputElement.setAttribute('checked', value.replace('true', 'on'));
if (value === 'true') {
inputElement.setAttribute('checked', '');
inputElement.setAttribute('value', value);
inputElement.value = value;
inputElement.checked = true;
} else {
inputElement.removeAttribute('checked');
inputElement.checked = false;
}
} else {
inputElement.value = value;
inputElement.setAttribute('value', value);
}
}

Expand Down Expand Up @@ -62,8 +68,7 @@ export function setDataToForm(form: HTMLFormElement, data: any) {
}

export function validateFormData<TSchema extends z.Schema>(formData: FormData, schema: TSchema) {
const object = preprocess(formDataToObject(formData), schema);
return schema.safeParse(object);
return schema.safeParse(formDataToObject(formData, schema));
}

export function clearFormValidationErrors(form: HTMLFormElement) {
Expand Down
13 changes: 7 additions & 6 deletions src/tests/form-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
setValidationErrorsToForm,
validateFormData
} from '../form-validation.js';
import { formDataToData } from '../utils.js';
import { formDataToObject } from '../utils.js';
import { EXPECTED_DATA, SCHEMA, getDom, getFormData } from './test-fixtures.js';

describe('Form Validation', () => {
Expand All @@ -19,7 +19,7 @@ describe('Form Validation', () => {
setFormDataToForm(form!, getFormData());
const formData = new FormData(form!);

expect(formDataToData(formData, SCHEMA)).toStrictEqual(EXPECTED_DATA);
expect(formDataToObject(formData, SCHEMA)).toStrictEqual(EXPECTED_DATA);

const arraySchema = z.object({
array: z.array(z.number())
Expand All @@ -39,11 +39,11 @@ describe('Form Validation', () => {

setFormDataToForm(arrayFrom!, arrayFormDataA);
arrayFormDataA = new FormData(arrayFrom!);
expect(formDataToData(arrayFormDataA, arraySchema)).toStrictEqual({ array: arrayValuesA });
expect(formDataToObject(arrayFormDataA, arraySchema)).toStrictEqual({ array: arrayValuesA });

setFormDataToForm(arrayFrom!, arrayFormDataB);
arrayFormDataB = new FormData(arrayFrom!);
expect(formDataToData(arrayFormDataB, arraySchema)).toStrictEqual({ array: arrayValuesB });
expect(formDataToObject(arrayFormDataB, arraySchema)).toStrictEqual({ array: arrayValuesB });
});

test('setDataToForm', () => {
Expand All @@ -53,7 +53,7 @@ describe('Form Validation', () => {

setDataToForm(form!, EXPECTED_DATA);
const formData = new FormData(form!);
expect(formDataToData(formData, SCHEMA)).toStrictEqual(EXPECTED_DATA);
expect(formDataToObject(formData, SCHEMA)).toStrictEqual(EXPECTED_DATA);
});

test('validateFormData', () => {
Expand All @@ -80,6 +80,7 @@ describe('Form Validation', () => {
expect(validationFailedResult.success).toBe(false);
if (validationFailedResult.success === false) {
setValidationErrorsToForm(form!, validationFailedResult.error);

const inputs = form?.querySelectorAll('[data-validation-error=true]');
expect([...inputs!].length).toBe(11);
}
Expand All @@ -91,6 +92,6 @@ describe('Form Validation', () => {
setRequiresToForm(form!, SCHEMA);
const inputs = form?.querySelectorAll('[data-validation-required]');
expect(inputs).not.toBe(null);
expect([...inputs!].length).toBe(11);
expect([...inputs!].length).toBe(13);
});
});
13 changes: 9 additions & 4 deletions src/tests/test-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import { z } from 'zod';
export const SCHEMA = z
.object({
string: z.string().optional(),
toc: z.boolean().refine(val => val === true, {
message: 'Please read and accept the terms and conditions'
}),
boolean: z.boolean(),
defaultBoolean: z.boolean().default(true),
defaultBoolean: z.boolean(),
bigInt: z.bigint(),
number: z.number(),
stringArray: z.array(z.string()),
Expand All @@ -26,9 +29,10 @@ export const SCHEMA = z
export type DataType = z.infer<typeof SCHEMA>;

export const EXPECTED_DATA: DataType = {
toc: true,
bigInt: 1n,
bigIntArray: [0n, 1n, 2n],
boolean: true,
boolean: false,
defaultBoolean: true,
booleanArray: [true, false],
number: 1024,
Expand All @@ -54,8 +58,9 @@ export function getDom() {
<option value="1">1</option>
<option value="2">2</option>
</select>
<input name="toc" type="checkbox" />
<input name="boolean" type="checkbox" />
<input name="defaultBoolean" type="checkbox" />
<input name="defaultBoolean" type="checkbox"/>
<select multiple name="booleanArray">
<option value="true">true</option>
<option value="false">false</option>
Expand Down Expand Up @@ -92,11 +97,11 @@ export function getDom() {
export function getFormData() {
const formData = new FormData();

formData.append('toc', 'true');
formData.append('bigInt', '1');
formData.append('bigIntArray', '0');
formData.append('bigIntArray', '1');
formData.append('bigIntArray', '2');
formData.append('boolean', 'true');
formData.append('defaultBoolean', 'true');
formData.append('booleanArray', 'true');
formData.append('booleanArray', 'false');
Expand Down
54 changes: 10 additions & 44 deletions src/tests/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,19 @@
import { describe, expect, test } from 'vitest';
import { z } from 'zod';
import {
flattenObject,
flattenSchema,
formDataToObject,
objectToFormData,
parseFormData,
preprocess
} from '../utils.js';
import { flattenObject, flattenSchema, formDataToObject, objectToFormData, parseFormData } from '../utils.js';
import { EXPECTED_DATA, SCHEMA, getFormData } from './test-fixtures.js';

describe('utils', () => {
test('formDataToObject', () => {
const formData = getFormData();
const object = formDataToObject(formData);
expect(object).toStrictEqual({
bigInt: ['1'],
bigIntArray: ['0', '1', '2'],
boolean: ['true'],
defaultBoolean: ['true'],
booleanArray: ['true', 'false'],
number: ['1024'],
numberArray: ['0'],
string: ['string'],
stringArray: ['0', '1', '2'],
enum: ['ONE'],
object: {
string: ['string'],
numberArray: ['0', '1', '2'],
nested: { string: ['string'] }
}
});
const object = formDataToObject(formData, SCHEMA);
expect(object).toStrictEqual(EXPECTED_DATA);
});

test('objectToFormData', () => {
const formData = getFormData();
const object = formDataToObject(formData);
expect(formDataToObject(objectToFormData(object))).toStrictEqual(object);
const object = formDataToObject(formData, SCHEMA);
expect(formDataToObject(objectToFormData(object), SCHEMA)).toStrictEqual(object);

const undefinedFormData = objectToFormData({
unused: undefined
Expand All @@ -47,7 +24,7 @@ describe('utils', () => {

test('flattenObject', () => {
const formData = getFormData();
const object = formDataToObject(formData);
const object = formDataToObject(formData, SCHEMA);
const flatObject = flattenObject(object);

expect(Object.keys(flatObject).sort()).toStrictEqual(
Expand All @@ -64,22 +41,10 @@ describe('utils', () => {
'enum',
'object.string',
'object.numberArray',
'object.nested.string'
'object.nested.string',
'toc'
].sort()
);

const allElementsAreArrays = Object.values(flatObject)
.map(value => Array.isArray(value))
.reduce((acc, value) => acc && value, true);

expect(allElementsAreArrays).toBe(true);
});

test('preprocess', () => {
const formData = getFormData();
const formDataObject = formDataToObject(formData);
const object = preprocess(formDataObject, SCHEMA);
expect(object).toStrictEqual(EXPECTED_DATA);
});

test('flattenSchema', () => {
Expand All @@ -100,7 +65,8 @@ describe('utils', () => {
'object.nested',
'object.string',
'object.numberArray',
'object.nested.string'
'object.nested.string',
'toc'
].sort()
);
});
Expand Down
Loading

0 comments on commit 6d8631f

Please sign in to comment.