Skip to content

Commit

Permalink
✨ Added basic data/form validation with zod
Browse files Browse the repository at this point in the history
  • Loading branch information
Luke Carr committed Apr 8, 2022
1 parent 5b50a65 commit 9bb2d09
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 18 deletions.
9 changes: 7 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@
"lint": "eslint --ext .ts,tsx --ignore-path .gitignore ."
},
"dependencies": {
"@hookform/resolvers": "2.8.8",
"clsx": "1.1.1",
"immer": "9.0.12",
"ohmyfetch": "0.4.15",
"preact": "10.7.1",
"preact-async-route": "2.2.1",
"preact-router": "4.0.1",
"react-hook-form": "7.29.0",
"swr": "1.2.2",
"use-immer": "0.6.0"
"use-immer": "0.6.0",
"zod": "3.14.4"
},
"devDependencies": {
"@preact/preset-vite": "2.2.0",
Expand Down Expand Up @@ -48,7 +51,9 @@
},
"rules": {},
"settings": {
"jest": { "version": 27 }
"jest": {
"version": 27
}
}
},
"volta": {
Expand Down
23 changes: 23 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 14 additions & 7 deletions frontend/src/lib/task.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { $fetch } from 'ohmyfetch'
import { z } from 'zod'

export type Task = {
/**
Expand All @@ -17,19 +18,25 @@ export type Task = {
complete: boolean
}

export const TaskSchema = z.object({
name: z.string().min(1, 'Task name cannot be empty!'),
})

export type TaskInput = z.infer<typeof TaskSchema>

/**
* Creates a new task by sending a POST request to the `/api/tasks` endpoint.
*
* @param input The new task to create.
* @param task The new task to create.
* @returns The newly created task.
*/
export async function createTask({ name }: Omit<Task, 'id' | 'complete'>): Promise<Task> {
const task = await $fetch<Task>('/api/tasks', {
export async function createTask(task: TaskInput): Promise<Task> {
const body = TaskSchema.parse(task)

const created = await $fetch<Task>('/api/tasks', {
method: 'POST',
body: {
name,
},
body,
})

return task
return created
}
1 change: 1 addition & 0 deletions frontend/src/routes/about.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const About: FunctionalComponent = () => {
<li>Styled with WindiCSS.</li>
<li>Data fetching and caching: swr and ohmyfetch</li>
<li>Form handling: react-hook-form</li>
<li>Validation: zod</li>
</ul>
</ul>
</>
Expand Down
19 changes: 10 additions & 9 deletions frontend/src/routes/todos.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import clsx from 'clsx'
import { useEffect } from 'preact/hooks'
import { SubmitHandler, useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import useSWR, { mutate } from 'swr'

import { createTask, Task } from 'src/lib/task'
import { createTask, Task, TaskInput, TaskSchema } from 'src/lib/task'

import type { FunctionalComponent } from 'preact'

Expand All @@ -17,10 +19,6 @@ const Tasks: FunctionalComponent = () => {
</ul>
}

type NewTaskInputs = {
name: string
}

const SubmitBtn: FunctionalComponent = () => <input
bg="gray-700 hover:black"
text="white"
Expand All @@ -33,15 +31,17 @@ const SubmitBtn: FunctionalComponent = () => <input
/>

const NewTask: FunctionalComponent = () => {
const { register, handleSubmit, reset } = useForm<NewTaskInputs>()
const { register, handleSubmit, reset, formState: { errors } } = useForm<TaskInput>({
resolver: zodResolver(TaskSchema),
})

/**
* Attempts to create a new task, and then mutates the SWR cache and resets
* the form inputs.
*
* @param task The new task to create.
*/
const create: SubmitHandler<NewTaskInputs> = async ({ name }) => {
const create: SubmitHandler<TaskInput> = async ({ name }) => {
try {
await createTask({ name })
mutate('/tasks')
Expand All @@ -55,14 +55,15 @@ const NewTask: FunctionalComponent = () => {
return <form onSubmit={handleSubmit(create)} class="mb-8 space-y-4">
<input
block="~"
border="gray-200 focus:gray-400 1"
border={clsx(typeof errors.name === 'undefined' ? 'gray-200 focus:gray-400' : 'red-400 focus:red-600', '1')}
w="full"
p="x-4 y-2"
rounded="sm"
outline="focus:none"
placeholder="Use tiny-todo everyday!"
{...register('name', { required: true })}
{...register('name')}
/>
{errors.name?.message && <p text="xs red-500" font="semibold">{errors.name.message}</p>}
<SubmitBtn />
</form>
}
Expand Down

0 comments on commit 9bb2d09

Please sign in to comment.