Getting started
Forms and validation
A form sends data to a controller. When the data is valid you save it and redirect, and when it isn't, the errors show up next to the fields.
The controller
Validate with a DTO like you would in any NestJS app, then save and redirect.
// src/users/create-user.dto.ts
import { IsEmail, IsNotEmpty } from 'class-validator'
export class CreateUserDto {
@IsNotEmpty({ message: 'Please enter a name.' })
name: string
@IsEmail({}, { message: 'That is not an email address.' })
email: string
}
// src/users/users.controller.ts
import { Body, Controller, Get, Post } from '@nestjs/common'
import { View, ViewService } from 'nestjs-mvc'
import { CreateUserDto } from './create-user.dto'
@Controller('users')
export class UsersController {
constructor(
private readonly users: UsersService,
private readonly view: ViewService,
) {}
@Get('create')
@View('Users/Create')
create() {
return {}
}
@Post()
async store(@Body() dto: CreateUserDto) {
await this.users.create(dto)
return this.view.redirect('/users')
}
}
You'll notice there's no code for when validation fails. nestjs-mvc sends the user back to the form with the errors, thanks to the validationExceptionFactory you added during installation.
The page
useForm holds the form data, sends it and gives you the errors back.
// frontend/pages/Users/Create.tsx
import { useForm } from 'nestjs-mvc/react'
export default function Create() {
const form = useForm({ name: '', email: '' })
function submit(e: React.FormEvent) {
e.preventDefault()
form.post('/users')
}
return (
<form onSubmit={submit}>
<input value={form.data.name} onChange={(e) => form.setData('name', e.target.value)} />
{form.errors.name && <p>{form.errors.name}</p>}
<input value={form.data.email} onChange={(e) => form.setData('email', e.target.value)} />
{form.errors.email && <p>{form.errors.email}</p>}
<button disabled={form.processing}>Save</button>
</form>
)
}
<!-- frontend/pages/Users/Create.vue -->
<script setup lang="ts">
import { useForm } from 'nestjs-mvc/vue'
const form = useForm({ name: '', email: '' })
</script>
<template>
<form @submit.prevent="form.post('/users')">
<input v-model="form.name" />
<p v-if="form.errors.name">{{ form.errors.name }}</p>
<input v-model="form.email" />
<p v-if="form.errors.email">{{ form.errors.email }}</p>
<button :disabled="form.processing">Save</button>
</form>
</template>
form.dataholds the values, andform.setDatachanges one.
- The values are properties of the form itself, so
v-model="form.name"binds an input to one.
form.post(url)sends them, and there's alsoform.put,form.patchandform.delete.form.errorsholds one message per field.form.processingistruewhile the request runs.
Checks in your handler
Some rules need the database, like "this email is already taken". For those, throw a ValidationException from your handler or service, and the user gets the error the same way as with a failed DTO:
import { ValidationException } from 'nestjs-mvc'
@Post()
async store(@Body() dto: CreateUserDto) {
if (await this.users.emailTaken(dto.email)) {
throw new ValidationException({ email: 'That email is already taken.' })
}
await this.users.create(dto)
return this.view.redirect('/users')
}
After a success
When saving works, you might want to reset the form. You can also show a flash message.
form.post('/users', {
onSuccess: () => form.reset(),
})
Editing
For an edit form, start useForm with the current values and send a PUT:
export default function Edit({ user }: { user: { id: number; name: string; email: string } }) {
const form = useForm({ name: user.name, email: user.email })
function submit(e: React.FormEvent) {
e.preventDefault()
form.put(`/users/${user.id}`)
}
// ...the same fields as above
}
<script setup lang="ts">
import { useForm } from 'nestjs-mvc/vue'
const props = defineProps<{ user: { id: number; name: string; email: string } }>()
const form = useForm({ name: props.user.name, email: props.user.email })
</script>
<template>
<form @submit.prevent="form.put(`/users/${user.id}`)">
<!-- ...the same fields as above -->
</form>
</template>
@Put(':id')
async update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateUserDto) {
await this.users.update(id, dto)
return this.view.redirect(`/users/${id}`)
}
Going further
Validation picks up from here: rules written with Zod, nested fields and lists, messages that belong to no field, two forms on one page, and what clients other than your pages get back.
In detail
Where the user is sent back
The user goes back to the page they came from, and what they typed is still in the form. The errors are there for one page only: the next visit starts with an empty errors again. On the page, a failed submit calls onError, never onSuccess, so the form.reset() above doesn't wipe the form.
What someone typed is lost when they leave the page, though. To keep it for when they come back with the back button, give the form a key, as in Remembering state.