Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Form): handle @error event #718

Merged
merged 15 commits into from
Oct 10, 2023
Merged
10 changes: 3 additions & 7 deletions docs/components/content/examples/FormExampleBasic.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { FormError, FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
import type { FormError, FormSubmitEvent } from '#ui/types'

const state = reactive({
email: undefined,
Expand All @@ -13,18 +13,14 @@ const validate = (state: any): FormError[] => {
return errors
}

async function submit (event: FormSubmitEvent<any>) {
async function onSubmit (event: FormSubmitEvent<any>) {
// Do something with data
console.log(event.data)
}
</script>

<template>
<UForm
:validate="validate"
:state="state"
@submit="submit"
>
<UForm :validate="validate" :state="state" @submit="onSubmit">
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>
Expand Down
11 changes: 3 additions & 8 deletions docs/components/content/examples/FormExampleElements.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { z } from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
import type { FormSubmitEvent } from '#ui/types'

const options = [
{ label: 'Option 1', value: 'option-1' },
Expand Down Expand Up @@ -45,19 +45,14 @@ type Schema = z.infer<typeof schema>

const form = ref()

async function submit (event: FormSubmitEvent<Schema>) {
async function onSubmit (event: FormSubmitEvent<Schema>) {
// Do something with event.data
console.log(event.data)
}
</script>

<template>
<UForm
ref="form"
:schema="schema"
:state="state"
@submit="submit"
>
<UForm ref="form" :schema="schema" :state="state" @submit="onSubmit">
<UFormGroup name="input" label="Input">
<UInput v-model="state.input" />
</UFormGroup>
Expand Down
10 changes: 3 additions & 7 deletions docs/components/content/examples/FormExampleJoi.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import Joi from 'joi'
import type { FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
import type { FormSubmitEvent } from '#ui/types'

const schema = Joi.object({
email: Joi.string().required(),
Expand All @@ -14,18 +14,14 @@ const state = reactive({
password: undefined
})

async function submit (event: FormSubmitEvent<any>) {
async function onSubmit (event: FormSubmitEvent<any>) {
// Do something with event.data
console.log(event.data)
}
</script>

<template>
<UForm
:schema="schema"
:state="state"
@submit="submit"
>
<UForm :schema="schema" :state="state" @submit="onSubmit">
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>
Expand Down
42 changes: 42 additions & 0 deletions docs/components/content/examples/FormExampleOnError.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script setup lang="ts">
import type { FormError, FormErrorEvent, FormSubmitEvent } from '#ui/types'

const state = reactive({
email: undefined,
password: undefined
})

const validate = (state: any): FormError[] => {
const errors = []
if (!state.email) errors.push({ path: 'email', message: 'Required' })
if (!state.password) errors.push({ path: 'password', message: 'Required' })
return errors
}

async function onSubmit (event: FormSubmitEvent<any>) {
// Do something with data
console.log(event.data)
}

async function onError (event: FormErrorEvent) {
const element = document.getElementById(event.errors[0].id)
element?.focus()
element?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
</script>

<template>
<UForm :validate="validate" :state="state" @submit="onSubmit" @error="onError">
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>

<UFormGroup label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormGroup>

<UButton type="submit">
Submit
</UButton>
</UForm>
</template>
10 changes: 3 additions & 7 deletions docs/components/content/examples/FormExampleValibot.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { string, objectAsync, email, minLength, Input } from 'valibot'
import type { FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
import type { FormSubmitEvent } from '#ui/types'

const schema = objectAsync({
email: string([email('Invalid email')]),
Expand All @@ -14,18 +14,14 @@ const state = reactive({
password: undefined
})

async function submit (event: FormSubmitEvent<Schema>) {
async function onSubmit (event: FormSubmitEvent<Schema>) {
// Do something with event.data
console.log(event.data)
}
</script>

<template>
<UForm
:schema="schema"
:state="state"
@submit="submit"
>
<UForm :schema="schema" :state="state" @submit="onSubmit">
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>
Expand Down
10 changes: 3 additions & 7 deletions docs/components/content/examples/FormExampleYup.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { object, string, InferType } from 'yup'
import type { FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
import type { FormSubmitEvent } from '#ui/types'

const schema = object({
email: string().email('Invalid email').required('Required'),
Expand All @@ -16,18 +16,14 @@ const state = reactive({
password: undefined
})

async function submit (event: FormSubmitEvent<Schema>) {
async function onSubmit (event: FormSubmitEvent<Schema>) {
// Do something with event.data
console.log(event.data)
}
</script>

<template>
<UForm
:schema="schema"
:state="state"
@submit="submit"
>
<UForm :schema="schema" :state="state" @submit="onSubmit">
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>
Expand Down
10 changes: 3 additions & 7 deletions docs/components/content/examples/FormExampleZod.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { z } from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
import type { FormSubmitEvent } from '#ui/types'

const schema = z.object({
email: z.string().email('Invalid email'),
Expand All @@ -14,18 +14,14 @@ const state = reactive({
password: undefined
})

async function submit (event: FormSubmitEvent<Schema>) {
async function onSubmit (event: FormSubmitEvent<Schema>) {
// Do something with data
console.log(event.data)
}
</script>

<template>
<UForm
:schema="schema"
:state="state"
@submit="submit"
>
<UForm :schema="schema" :state="state" @submit="onSubmit">
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>
Expand Down
1 change: 1 addition & 0 deletions docs/content/1.getting-started/5.examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ Our theming system provides a lot of flexibility to customize the components.
Here is some examples of what you can do with the [CommandPalette](/navigation/command-palette).

#### Algolia

::component-example
---
padding: false
Expand Down
19 changes: 15 additions & 4 deletions docs/content/3.forms/10.form.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ You can provide a schema from [Yup](#yup), [Zod](#zod) or [Joi](#joi), [Valibot]

:component-example{component="form-example-joi" :componentProps='{"class": "space-y-4 w-60"}'}


### Valibot

:component-example{component="form-example-valibot" :componentProps='{"class": "space-y-4 w-60"}'}
Expand Down Expand Up @@ -87,7 +86,7 @@ You can manually set errors after form submission if required. To do this, simpl

```vue
<script setup lang="ts">
import type { FormError, FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
import type { FormError, FormSubmitEvent } from '#ui/types'

const state = reactive({
email: undefined,
Expand All @@ -96,7 +95,7 @@ const state = reactive({

const form = ref()

async function submit (event: FormSubmitEvent<any>) {
async function onSubmit (event: FormSubmitEvent<any>) {
form.value.clear()
try {
const response = await $fetch('...')
Expand All @@ -112,7 +111,7 @@ async function submit (event: FormSubmitEvent<any>) {
</script>

<template>
<UForm ref="form" :state="state" @submit="submit">
<UForm ref="form" :state="state" @submit="onSubmit">
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>
Expand All @@ -138,6 +137,18 @@ The Form component automatically triggers validation upon `submit`, `input`, `bl
Take a look at the component!
::

## Error event :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}

You can listen to the `@error` event to handle errors. This event is triggered when the form is validated and contains an array of `FormError` objects with the following fields:

- `id` - the identifier of the form element.
- `path` - the path to the form element matching the `name`.
- `message` - the error message to display.

Here is an example of how to focus the first form element with an error:

:component-example{component="form-example-on-error" :componentProps='{"class": "space-y-4 w-60"}'}

## Props

:component-props
Expand Down
43 changes: 35 additions & 8 deletions src/runtime/components/forms/Form.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ import type { ZodSchema } from 'zod'
import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } from 'yup'
import type { ObjectSchemaAsync as ValibotObjectSchema } from 'valibot'
import type { FormError, FormEvent, FormEventType, FormSubmitEvent, Form } from '../../types/form'
import type { FormError, FormEvent, FormEventType, FormSubmitEvent, FormErrorEvent, Form } from '../../types/form'
import { uid } from '../../utils/uid'

class FormException extends Error {
constructor (message: string) {
super(message)
this.message = message
Object.setPrototypeOf(this, FormException.prototype)
}
}

export default defineComponent({
props: {
schema: {
Expand All @@ -39,7 +47,7 @@ export default defineComponent({
default: () => ['blur', 'input', 'change', 'submit']
}
},
emits: ['submit'],
emits: ['submit', 'error'],
setup (props, { expose, emit }) {
const bus = useEventBus<FormEvent>(`form-${uid()}`)

Expand All @@ -52,6 +60,8 @@ export default defineComponent({
const errors = ref<FormError[]>([])
provide('form-errors', errors)
provide('form-events', bus)
const inputs = ref({})
provide('form-inputs', inputs)

async function getErrors (): Promise<FormError[]> {
let errs = await props.validate(props.state)
Expand Down Expand Up @@ -87,20 +97,37 @@ export default defineComponent({
}

if (!opts.silent && errors.value.length > 0) {
throw new Error(
throw new FormException(
`Form validation failed: ${JSON.stringify(errors.value, null, 2)}`
)
}
return props.state
}

async function onSubmit (event: SubmitEvent) {
if (props.validateOn?.includes('submit')) {
await validate()
try {
if (props.validateOn?.includes('submit')) {
await validate()
}
const submitEvent: FormSubmitEvent<any> = {
...event,
data: props.state
}
emit('submit', submitEvent)
} catch (error) {
if (!(error instanceof FormException)) {
throw error
}

const errorEvent: FormErrorEvent = {
...event,
errors: errors.value.map((err) => ({
...err,
id: inputs.value[err.path]
}))
}
emit('error', errorEvent)
}
const submitEvent = event as FormSubmitEvent<any>
submitEvent.data = props.state
emit('submit', event)
}

expose({
Expand Down
Loading