Skip to content

Commit

Permalink
feat(Form): add superstruct validation (#2357)
Browse files Browse the repository at this point in the history
  • Loading branch information
rdjanuar authored Oct 11, 2024
1 parent 428ee44 commit 3cda6c6
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 3 deletions.
36 changes: 36 additions & 0 deletions docs/components/content/examples/FormExampleSuperstruct.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script setup lang="ts">
import { object, string, nonempty, type Infer } from 'superstruct'
import type { FormSubmitEvent } from '#ui/types'
const schema = object({
email: nonempty(string()),
password: nonempty(string())
})
const state = reactive({
email: '',
password: ''
})
type Schema = Infer<typeof schema>
async function onSubmit (event: FormSubmitEvent<Schema>) {
console.log(event.data)
}
</script>

<template>
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
<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>
11 changes: 9 additions & 2 deletions docs/content/2.components/form.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ links:

## Usage

Use the Form component to validate form data using schema libraries such as [Yup](https://github.com/jquense/yup), [Zod](https://github.com/colinhacks/zod), [Joi](https://github.com/hapijs/joi), [Valibot](https://github.com/fabian-hiller/valibot), or your own validation logic.
Use the Form component to validate form data using schema libraries such as [Yup](https://github.com/jquense/yup), [Zod](https://github.com/colinhacks/zod), [Joi](https://github.com/hapijs/joi), [Valibot](https://github.com/fabian-hiller/valibot), [Superstruct](https://github.com/ianstormtaylor/superstruct), or your own validation logic.

It works with the [FormGroup](/components/form-group) component to display error messages around form elements automatically.

The form component requires two props:
- `state` - a reactive object holding the form's state.
- `schema` - a schema object from a validation library like [Yup](https://github.com/jquense/yup), [Zod](https://github.com/colinhacks/zod), [Joi](https://github.com/hapijs/joi) or [Valibot](https://github.com/fabian-hiller/valibot).
- `schema` - a schema object from a validation library like [Yup](https://github.com/jquense/yup), [Zod](https://github.com/colinhacks/zod), [Joi](https://github.com/hapijs/joi), [Valibot](https://github.com/fabian-hiller/valibot) or [Superstruct](https://github.com/ianstormtaylor/superstruct).

::callout{icon="i-heroicons-light-bulb"}
Note that **no validation library is included** by default, so ensure you **install the one you need**.
Expand Down Expand Up @@ -52,6 +52,13 @@ Note that **no validation library is included** by default, so ensure you **inst
class: 'w-60'
---
::
::component-example{label="Superstruct"}
---
component: 'form-example-superstruct'
componentProps:
class: 'w-60'
---
::
::

## Custom validation
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"joi": "^17.13.3",
"nuxt": "^3.13.2",
"release-it": "^17.7.0",
"superstruct": "^2.0.2",
"unbuild": "^2.0.0",
"valibot": "^0.42.1",
"valibot30": "npm:[email protected]",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 26 additions & 1 deletion src/runtime/components/forms/Form.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } fro
import type { BaseSchema as ValibotSchema30, BaseSchemaAsync as ValibotSchemaAsync30 } from 'valibot30'
import type { GenericSchema as ValibotSchema31, GenericSchemaAsync as ValibotSchemaAsync31, SafeParser as ValibotSafeParser31, SafeParserAsync as ValibotSafeParserAsync31 } from 'valibot31'
import type { GenericSchema as ValibotSchema, GenericSchemaAsync as ValibotSchemaAsync, SafeParser as ValibotSafeParser, SafeParserAsync as ValibotSafeParserAsync } from 'valibot'
import type { Struct } from 'superstruct'
import type { FormError, FormEvent, FormEventType, FormSubmitEvent, FormErrorEvent, Form } from '../../types/form'
import { useId } from '#imports'
Expand All @@ -35,7 +36,7 @@ export default defineComponent({
| PropType<ValibotSchema31 | ValibotSchemaAsync31>
| PropType<ValibotSafeParser31<any, any> | ValibotSafeParserAsync31<any, any>>
| PropType<ValibotSchema | ValibotSchemaAsync>
| PropType<ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any>>,
| PropType<ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any>> | PropType<Struct<any, any>>,
default: undefined
},
state: {
Expand Down Expand Up @@ -88,6 +89,8 @@ export default defineComponent({
errs = errs.concat(await getJoiErrors(props.state, props.schema))
} else if (isValibotSchema(props.schema)) {
errs = errs.concat(await getValibotError(props.state, props.schema))
} else if (isSuperStructSchema(props.schema)) {
errs = errs.concat(await getSuperStructErrors(props.state, props.schema))
} else {
throw new Error('Form validation failed: Unsupported form schema')
}
Expand Down Expand Up @@ -195,6 +198,15 @@ function isYupError (error: any): error is YupError {
return error.inner !== undefined
}
function isSuperStructSchema (schema: any): schema is Struct<any, any> {
return (
'schema' in schema &&
typeof schema.coercer === 'function' &&
typeof schema.validator === 'function' &&
typeof schema.refiner === 'function'
)
}
async function getYupErrors (
state: any,
schema: YupObjectSchema<any>
Expand All @@ -218,6 +230,18 @@ function isZodSchema (schema: any): schema is ZodSchema {
return schema.parse !== undefined
}
async function getSuperStructErrors (state: any, schema: Struct<any, any>): Promise<FormError[]> {
const [err] = schema.validate(state)
if (err) {
const errors = err.failures()
return errors.map((error) => ({
message: error.message,
path: error.path.join('.')
}))
}
return []
}
async function getZodErrors (
state: any,
schema: ZodSchema
Expand Down Expand Up @@ -259,6 +283,7 @@ async function getJoiErrors (
}
}
function isValibotSchema (schema: any): schema is ValibotSchema30 | ValibotSchemaAsync30 | ValibotSchema31 | ValibotSchemaAsync31 | ValibotSafeParser31<any, any> | ValibotSafeParserAsync31<any, any> | ValibotSchema | ValibotSchemaAsync | ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any> {
return '_parse' in schema || '_run' in schema || (typeof schema === 'function' && 'schema' in schema)
}
Expand Down

0 comments on commit 3cda6c6

Please sign in to comment.