diff --git a/docs/app/components/content/ComponentExample.vue b/docs/app/components/content/ComponentExample.vue index e63273238c..805efb84b1 100644 --- a/docs/app/components/content/ComponentExample.vue +++ b/docs/app/components/content/ComponentExample.vue @@ -21,8 +21,26 @@ const props = withDefaults(defineProps<{ * @defaultValue true */ preview?: boolean + + /** + * Whether to show the source code + * @defaultValue true + */ + source?: boolean + + /** + * A list of variable props to link to the component. + */ + options?: Array<{ + name: string + label: string + items: any[] + default: any + multiple: boolean + }> }>(), { - preview: true + preview: true, + source: true }) const { $prettier } = useNuxtApp() @@ -71,16 +89,48 @@ const { data: ast } = await useAsyncData(`component-example-${camelName}`, async return parseMarkdown(formatted) }, { watch: [code] }) + +const optionsValues = ref(props.options?.reduce((acc, option) => { + if (option.name) { + acc[option.name] = option.default + } + return acc +}, {})) diff --git a/docs/app/components/content/examples/form/FormExampleBasic.vue b/docs/app/components/content/examples/form/FormExampleBasic.vue new file mode 100644 index 0000000000..be1469060f --- /dev/null +++ b/docs/app/components/content/examples/form/FormExampleBasic.vue @@ -0,0 +1,37 @@ + + + diff --git a/docs/app/components/content/examples/form/FormExampleElements.vue b/docs/app/components/content/examples/form/FormExampleElements.vue new file mode 100644 index 0000000000..671eee7a28 --- /dev/null +++ b/docs/app/components/content/examples/form/FormExampleElements.vue @@ -0,0 +1,115 @@ + + + diff --git a/docs/app/components/content/examples/form/FormExampleJoi.vue b/docs/app/components/content/examples/form/FormExampleJoi.vue new file mode 100644 index 0000000000..5967a15c00 --- /dev/null +++ b/docs/app/components/content/examples/form/FormExampleJoi.vue @@ -0,0 +1,38 @@ + + + diff --git a/docs/app/components/content/examples/form/FormExampleNested.vue b/docs/app/components/content/examples/form/FormExampleNested.vue new file mode 100644 index 0000000000..932eb34fd7 --- /dev/null +++ b/docs/app/components/content/examples/form/FormExampleNested.vue @@ -0,0 +1,54 @@ + + + diff --git a/docs/app/components/content/examples/form/FormExampleNestedList.vue b/docs/app/components/content/examples/form/FormExampleNestedList.vue new file mode 100644 index 0000000000..1ce8b1b532 --- /dev/null +++ b/docs/app/components/content/examples/form/FormExampleNestedList.vue @@ -0,0 +1,79 @@ + + + diff --git a/docs/app/components/content/examples/form/FormExampleOnError.vue b/docs/app/components/content/examples/form/FormExampleOnError.vue new file mode 100644 index 0000000000..26b0cf6acf --- /dev/null +++ b/docs/app/components/content/examples/form/FormExampleOnError.vue @@ -0,0 +1,43 @@ + + + diff --git a/docs/app/components/content/examples/form/FormExampleValibot.vue b/docs/app/components/content/examples/form/FormExampleValibot.vue new file mode 100644 index 0000000000..6d4ba282f0 --- /dev/null +++ b/docs/app/components/content/examples/form/FormExampleValibot.vue @@ -0,0 +1,38 @@ + + + diff --git a/docs/app/components/content/examples/form/FormExampleYup.vue b/docs/app/components/content/examples/form/FormExampleYup.vue new file mode 100644 index 0000000000..be288621ab --- /dev/null +++ b/docs/app/components/content/examples/form/FormExampleYup.vue @@ -0,0 +1,40 @@ + + + diff --git a/docs/app/components/content/examples/form/FormExampleZod.vue b/docs/app/components/content/examples/form/FormExampleZod.vue new file mode 100644 index 0000000000..f838729bf3 --- /dev/null +++ b/docs/app/components/content/examples/form/FormExampleZod.vue @@ -0,0 +1,38 @@ + + + diff --git a/docs/content/2.composables/use-form-field.md b/docs/content/2.composables/use-form-field.md new file mode 100644 index 0000000000..c3ea455422 --- /dev/null +++ b/docs/content/2.composables/use-form-field.md @@ -0,0 +1,17 @@ +--- +title: useFormField +description: 'A composable to integrate custom inputs with the Form component' +navigation: + badge: + label: Todo +--- + +## Usage + +Use the auto-imported `useFormField` composable to integrate custom inputs with a [Form](/components/form). + +```vue + +``` diff --git a/docs/content/3.components/form.md b/docs/content/3.components/form.md index 7bfd2f6f1d..e334428c62 100644 --- a/docs/content/3.components/form.md +++ b/docs/content/3.components/form.md @@ -1,17 +1,155 @@ --- -description: A form element that provides validation and submission handling. +description: A form component with built-in validation and submission handling. links: - label: GitHub icon: i-simple-icons-github to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/Form.vue -navigation: - badge: - label: Todo --- ## Usage -## Examples +Use the Form component to validate form data using schema libraries such as [Zod](https://github.com/colinhacks/zod), [Yup](https://github.com/jquense/yup), [Joi](https://github.com/hapijs/joi), [Valibot](https://github.com/fabian-hiller/valibot), or your own validation logic. + +It works with the [FormField](/components/form-field) component to display error messages around form elements automatically. + +### Schema Validation + +It requires two props: +- `state` - a reactive object holding the form's state. +- `schema` - a schema object from a validation library like [Zod](https://github.com/colinhacks/zod), [Yup](https://github.com/jquense/yup), [Joi](https://github.com/hapijs/joi) or [Valibot](https://github.com/fabian-hiller/valibot). + +::warning +**No validation library is included** by default, ensure you **install the one you need**. +:: + +::tabs + ::component-example{label="Zod"} + --- + name: 'form-example-zod' + props: + class: 'w-60' + --- + :: + + ::component-example{label="Yup"} + --- + name: 'form-example-yup' + props: + class: 'w-60' + --- + :: + + ::component-example{label="Joi"} + --- + name: 'form-example-joi' + props: + class: 'w-60' + --- + :: + + ::component-example{label="Valibot"} + --- + name: 'form-example-valibot' + props: + class: 'w-60' + --- + :: +:: + +Errors are reported directly to the [FormField](/components/form-field) component based on the `name` prop. This means the validation rules defined for the `email` attribute in your schema will be applied to ``{lang="vue"}. + +Nested validation rules are handled using dot notation. For example, a rule like `{ user: z.object({ email: z.string() }) }`{lang="ts"} will be applied to ``{lang="vue"}. + +### Custom Validation + +Use the `validate` prop to apply your own validation logic. + +The validation function must return a list of errors with the following attributes: +- `message` - the error message to display. +- `name` - the `name` of the `FormField` to send the error to. + +::tip +It can be used alongside the `schema` prop to handle complex use cases. +:: + +::component-example +--- +name: 'form-example-basic' +props: + class: 'w-60' +--- +:: + +### Input Events + +The Form component automatically triggers validation when an input emits an `input`, `change`, or `blur` event. +- Validation on `input` occurs **as you type**. +- Validation on `change` occurs when you **commit to a value**. +- Validation on `blur` happens when an input **loses focus**. + +You can control when validation happens this using the `validate-on` prop. + +::component-example{label="Default"} +--- +source: false +name: 'form-example-elements' +options: + - name: 'validate-on' + label: 'validate-on' + items: + - 'input' + - 'change' + - 'blur' + default: + - 'input' + - 'change' + - 'blur' + multiple: true +--- +:: + +::tip +You can use the [useFormField](/composables/use-form-field) composable to implement this inside your own components. +:: + +### Error Event + +You can listen to the `@error` event to handle errors. This event is triggered when the form is submitted and contains an array of `FormError` objects with the following fields: + +- `id` - the input's `id`. +- `name` - the `name` of the `FormField` +- `message` - the error message to display. + +Here's an example that focuses the first input element with an error after the form is submitted: + +::component-example +--- +name: 'form-example-on-error' +collapse: true +props: + class: 'w-60' +--- +:: + +### Nesting Forms + +Nesting form components allows you to manage complex data structures, such as lists or conditional fields, more efficiently. + +For example, it can be used to dynamically add fields based on user's input: +::component-example +--- +collapse: true +name: 'form-example-nested' +--- +:: + +Or to validate list inputs: +::component-example +--- +collapse: true +name: 'form-example-nested-list' +--- +:: ## API diff --git a/src/runtime/components/Form.vue b/src/runtime/components/Form.vue index ee824ad7a3..f621d6381e 100644 --- a/src/runtime/components/Form.vue +++ b/src/runtime/components/Form.vue @@ -147,7 +147,8 @@ async function _validate(opts: { name?: string | string[], silent?: boolean, nes errors.value = await getErrors() } - const childErrors = nestedValidatePromises ? await Promise.all(nestedValidatePromises) : [] + const childErrors = (await Promise.all(nestedValidatePromises)).filter(val => val) + if (errors.value.length + childErrors.length > 0) { if (opts.silent) return false throw new FormValidationException(formId, errors.value, childErrors) @@ -173,7 +174,6 @@ async function onSubmit(payload: Event) { errors: error.errors, childrens: error.childrens } - emits('error', errorEvent) } }