mirror of
https://git.hmsn.ink/kospo/svcm/oa.git
synced 2026-03-20 01:12:30 +09:00
first
This commit is contained in:
@@ -0,0 +1,471 @@
|
||||
### 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-->
|
||||
Reference in New Issue
Block a user