mirror of
https://git.hmsn.ink/kospo/svcm/oa.git
synced 2026-03-20 06:33:32 +09:00
472 lines
12 KiB
Markdown
472 lines
12 KiB
Markdown
### VeeValidate and Zod
|
|
|
|
[VeeValidate](https://vee-validate.logaretm.com/v4/) offer a great set of tools
|
|
to validate your forms wich work perfectly with vuero.
|
|
We recommand you to use [zod](https://github.com/colinhacks/zod) to
|
|
declare schema for VeeValidate forms but you can also use [yup]()
|
|
if you already are familiar with it.
|
|
|
|
<!--code-->
|
|
|
|
```vue
|
|
<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>
|
|
```
|
|
|
|
<!--/code-->
|