mirror of
https://git.hmsn.ink/kospo/svcm/oa.git
synced 2026-03-20 06:33:32 +09:00
12 KiB
12 KiB
VeeValidate and Zod
VeeValidate offer a great set of tools to validate your forms wich work perfectly with vuero. We recommand you to use zod to declare schema for VeeValidate forms but you can also use yup if you already are familiar with it.
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { useField, useFieldArray, useForm } from 'vee-validate'
import { z } from 'zod'
import VueScrollTo from 'vue-scrollto'
const notyf = useNotyf()
const { scrollTo } = VueScrollTo
// This is the Zod schema for the form input
// It's used to define the shape that the form data will have
const zodSchema = z
.object({
email: z
.string({
required_error: 'Enter your email first',
})
.email('A valid email address should be provided'),
rating: z
.number({
required_error: 'Enter a valid rating first',
})
.gte(1, 'The rating should be at least 1'),
password: z
.string({
required_error: 'Enter your password to sign in',
})
.min(8, 'Your password should contains at least 8 characters'),
passwordCheck: z.string(),
birthdate: z
.date({
invalid_type_error: 'Please enter a valid date',
required_error: 'Please enter a date',
})
.max(new Date(), 'You cannot be born in the future')
.nullable(),
agreeTerms: z
.boolean()
.refine(value => value, 'You must agree our terms of service'),
area: z.object({
timezone: z.string().min(1, 'Please select a timezone'),
}),
interests: z
.string()
.array()
.min(2, 'You must select at least 2 center of interest')
.max(3, 'You can select up to 3 center of interest'),
allergens: z.string().array().max(4, 'You can select up to 4 allergen'),
feedback: z
.array(
z.object({
title: z
.string()
.min(10, 'Your experience title should be at least 10 characters'),
rating: z.number().gte(1, 'The rating should be at least 1'),
}),
)
.min(1, 'You must send at least 1 feedback')
.max(3, 'You can send up to 3 feedbacks'),
emailOptin: z.boolean(),
})
.refine(data => data.password === data.passwordCheck, {
message: 'The confirmation does not match the password',
path: ['passwordCheck'],
})
// Zod has a great infer method that will
// infer the shape of the schema into a TypeScript type
type FormInput = z.infer<typeof zodSchema>
// we need to declare the schema for the form
const validationSchema = toTypedSchema(zodSchema)
// Set initial values for the form
const initialValues = computed<FormInput>(() => ({
email: '',
password: '',
rating: 1,
passwordCheck: '',
birthdate: null,
interests: [],
allergens: [],
feedback: [],
agreeTerms: false,
emailOptin: false,
}))
// here we create a vee-validate form context that
// will be used in all vuero form components
const { handleSubmit, setFieldError, handleReset, values, errors } = useForm({
validationSchema,
initialValues,
})
const { remove, push, fields } = useFieldArray<FormInput['feedback'][0]>('feedback')
const { errorMessage } = useField<FormInput['feedback'][0]>('feedback')
const loading = ref(false)
// here we handle our form submission
const handleSignup = handleSubmit(async (values) => {
if (loading.value) return
loading.value = true
await sleep(1600)
if (values.email !== 'awesome@cssninja.io') {
setFieldError('email', 'This email is already taken! Tip: use awesome@cssninja.io')
scrollTo('#email')
loading.value = false
return
}
notyf.primary('You have successfully signed up!')
loading.value = false
})
</script>
<template>
<form
method="post"
novalidate
@submit.prevent="handleSignup"
>
<VField
id="email"
v-slot="{ field }"
label="Your email"
>
<VControl icon="lucide:user">
<VInput
type="email"
placeholder="john.doe@gmail.com"
autocomplete="username"
/>
<p v-if="field?.errorMessage" class="help is-danger">
{{ field.errorMessage }}
</p>
</VControl>
</VField>
<VField
id="password"
v-slot="{ field }"
label="Choose a password"
>
<VControl icon="lucide:lock">
<VInput
type="password"
placeholder="Not$3cret"
autocomplete="new-password"
/>
<p v-if="field?.errorMessage" class="help is-danger">
{{ field.errorMessage }}
</p>
</VControl>
</VField>
<VField
id="passwordCheck"
v-slot="{ field }"
label="Confirm your new password"
>
<VControl icon="lucide:check">
<VInput
type="password"
placeholder="Not$3cret"
autocomplete="new-password"
/>
<p v-if="field?.errorMessage" class="help is-danger">
{{ field.errorMessage }}
</p>
</VControl>
</VField>
<VField
id="birthdate"
v-slot="{ field }"
label="Birthdate"
>
<VControl icon="lucide:calendar">
<ClientOnly>
<VDatePicker
:model-value="field?.value"
color="green"
trim-weeks
@update:model-value="field?.handleChange"
>
<template #default="{ inputValue, inputEvents }">
<input
class="input"
type="text"
:value="inputValue"
placeholder="Select your birthdate"
v-on="inputEvents"
>
<p v-if="field?.errorMessage" class="help is-danger">
{{ field.errorMessage }}
</p>
</template>
</VDatePicker>
</ClientOnly>
</VControl>
</VField>
<VField
id="interests"
v-slot="{ field }"
class="pb-4"
label="Choose 2 or 3 center of interests"
>
<VControl>
<VSelect multiple size="9">
<VOption value="Food">
Food
</VOption>
<VOption value="Home Appliances">
Home Appliances
</VOption>
<VOption value="Computer & Office">
Computer & Office
</VOption>
<VOption value="Home Improvement">
Home Improvement
</VOption>
<VOption value="Home & Garden">
Home & Garden
</VOption>
<VOption value="Sports & Entertainment">
Sports & Entertainment
</VOption>
<VOption value="Toys & Hobbies">
Education & Office Supplies
</VOption>
<VOption value="Security & Protection">
Security & Protection
</VOption>
<VOption value="Lights & Lighting">
Lights & Lighting
</VOption>
</VSelect>
<p v-if="field?.errorMessage" class="help is-danger">
{{ field.errorMessage }}
</p>
<p class="help">
Hold down the <kbd>Ctrl</kbd> (windows) / <kbd>Command</kbd> (Mac) button to
select multiple options.
</p>
</VControl>
</VField>
<VField
id="area"
v-slot="{ field }"
class="pb-4"
label="Choose your timezone"
>
<VControl>
<VSelect>
<VOption :value="{ timezone: 'europe/paris', label: 'Paris' }">
europe
</VOption>
<VOption :value="{ timezone: 'asia/tokyo', label: 'Tokyo' }">
asia
</VOption>
<VOption :value="{ timezone: 'america/new_york', label: 'New York' }">
america
</VOption>
<VOption :value="{ timezone: 'australia/sydney', label: 'Sydney' }">
australia
</VOption>
</VSelect>
<p v-if="field?.errorMessage" class="help is-danger">
{{ field.errorMessage }}
</p>
</VControl>
</VField>
<VField
id="allergens"
v-slot="{ field }"
label="Pick your allergens"
>
<VControl>
<VCheckbox
class="pl-0"
color="primary"
value="peanuts"
>
Peanuts
</VCheckbox>
<VCheckbox
id="allergens-milk"
color="primary"
value="milk"
>
Milk
</VCheckbox>
<VCheckbox
id="allergens-egg"
color="primary"
value="egg"
>
Egg
</VCheckbox>
<VCheckbox
id="allergens-fish"
color="primary"
value="fish"
>
Fish
</VCheckbox>
<VCheckbox
id="allergens-soybeans"
color="primary"
value="soybeans"
>
Soybeans
</VCheckbox>
</VControl>
<p v-if="field?.errorMessage" class="help is-danger">
{{ field.errorMessage }}
</p>
</VField>
<div class="py-4">
<!-- eslint-disable-next-line vue/require-v-for-key -->
<div v-for="(element, index) in fields" class="my-3">
<div class="columns">
<VField
:id="`feedback[${index}].title`"
v-slot="{ field }"
label="Name your experience"
class="column is-two-fifths"
>
<VControl>
<VInput
type="email"
placeholder="john.doe@gmail.com"
autocomplete="username"
/>
<p v-if="field?.errorMessage" class="help is-danger">
{{ field.errorMessage }}
</p>
</VControl>
</VField>
<VField
:id="`feedback[${index}].rating`"
v-slot="{ field }"
class="ml-4"
label="Give a rating"
>
<VControl>
<VRangeRating class="mt-5" size="medium" />
<p v-if="field?.errorMessage" class="help is-danger">
{{ field.errorMessage }}
</p>
</VControl>
</VField>
<VIconButton
class="is-remove"
:style="{}"
light
raised
circle
color="danger"
icon="lucide:trash-2"
@click="() => remove(index)"
/>
</div>
</div>
<div class="mb-5">
<VButton @click="() => push({ rating: 3, title: '' })">
Add feedback
</VButton>
<p v-if="errorMessage" class="help is-danger">
{{ errorMessage }}
</p>
</div>
</div>
<VField id="agreeTerms" v-slot="{ field }">
<VControl>
<VCheckbox paddingless>
I agree to the <a href="#">terms and conditions</a>
</VCheckbox>
<p v-if="field?.errorMessage" class="help is-danger">
{{ field.errorMessage }}
</p>
</VControl>
</VField>
<VField id="emailOptin">
<VControl>
<VCheckbox color="primary" paddingless>
I want to receive exclusive news and updates
</VCheckbox>
</VControl>
</VField>
<VButtons class="pt-4">
<VButton
:loading="loading"
type="submit"
color="primary"
>
Submit
</VButton>
<VButton type="reset" @click="handleReset">
Reset
</VButton>
</VButtons>
<div class="demo-code-wrapper">
<div class="demo-state">
<pre>{{ values }}</pre>
</div>
<div class="demo-state">
<pre>{{ errors }}</pre>
</div>
</div>
</form>
</template>
<style lang="scss" scoped>
.is-remove {
margin-inline-start: 1.5rem;
margin-top: 2.25rem;
}
.demo-code-wrapper {
display: flex;
flex-direction: column-reverse;
margin-top: 2rem;
overflow-x: auto;
.demo-code {
flex-grow: 1;
}
.demo-state {
// flex-grow: 1;
position: relative;
margin-bottom: 1.5rem;
max-width: 100%;
&::before {
position: absolute;
top: 0.6em;
inset-inline-end: 1em;
z-index: 2;
font-size: 0.8rem;
color: #888;
content: 'values';
}
}
}
@media only screen and (width <= 767px) {
.is-remove {
margin-inline-start: 1rem;
margin-top: 1em;
margin-bottom: 2.25rem;
}
}
</style>