Skip to content

Commit 3cda6c6

Browse files
authoredOct 11, 2024··
feat(Form): add superstruct validation (#2357)

File tree

5 files changed

+81
-3
lines changed

5 files changed

+81
-3
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<script setup lang="ts">
2+
import { object, string, nonempty, type Infer } from 'superstruct'
3+
import type { FormSubmitEvent } from '#ui/types'
4+
5+
const schema = object({
6+
email: nonempty(string()),
7+
password: nonempty(string())
8+
})
9+
10+
const state = reactive({
11+
email: '',
12+
password: ''
13+
})
14+
15+
type Schema = Infer<typeof schema>
16+
17+
async function onSubmit (event: FormSubmitEvent<Schema>) {
18+
console.log(event.data)
19+
}
20+
</script>
21+
22+
<template>
23+
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
24+
<UFormGroup label="Email" name="email">
25+
<UInput v-model="state.email" />
26+
</UFormGroup>
27+
28+
<UFormGroup label="Password" name="password">
29+
<UInput v-model="state.password" type="password" />
30+
</UFormGroup>
31+
32+
<UButton type="submit">
33+
Submit
34+
</UButton>
35+
</UForm>
36+
</template>

‎docs/content/2.components/form.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ links:
88

99
## Usage
1010

11-
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.
11+
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.
1212

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

1515
The form component requires two props:
1616
- `state` - a reactive object holding the form's state.
17-
- `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).
17+
- `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).
1818

1919
::callout{icon="i-heroicons-light-bulb"}
2020
Note that **no validation library is included** by default, so ensure you **install the one you need**.
@@ -52,6 +52,13 @@ Note that **no validation library is included** by default, so ensure you **inst
5252
class: 'w-60'
5353
---
5454
::
55+
::component-example{label="Superstruct"}
56+
---
57+
component: 'form-example-superstruct'
58+
componentProps:
59+
class: 'w-60'
60+
---
61+
::
5562
::
5663

5764
## Custom validation

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"joi": "^17.13.3",
6767
"nuxt": "^3.13.2",
6868
"release-it": "^17.7.0",
69+
"superstruct": "^2.0.2",
6970
"unbuild": "^2.0.0",
7071
"valibot": "^0.42.1",
7172
"valibot30": "npm:valibot@0.30.0",

‎pnpm-lock.yaml

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/runtime/components/forms/Form.vue

+26-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } fro
1313
import type { BaseSchema as ValibotSchema30, BaseSchemaAsync as ValibotSchemaAsync30 } from 'valibot30'
1414
import type { GenericSchema as ValibotSchema31, GenericSchemaAsync as ValibotSchemaAsync31, SafeParser as ValibotSafeParser31, SafeParserAsync as ValibotSafeParserAsync31 } from 'valibot31'
1515
import type { GenericSchema as ValibotSchema, GenericSchemaAsync as ValibotSchemaAsync, SafeParser as ValibotSafeParser, SafeParserAsync as ValibotSafeParserAsync } from 'valibot'
16+
import type { Struct } from 'superstruct'
1617
import type { FormError, FormEvent, FormEventType, FormSubmitEvent, FormErrorEvent, Form } from '../../types/form'
1718
import { useId } from '#imports'
1819
@@ -35,7 +36,7 @@ export default defineComponent({
3536
| PropType<ValibotSchema31 | ValibotSchemaAsync31>
3637
| PropType<ValibotSafeParser31<any, any> | ValibotSafeParserAsync31<any, any>>
3738
| PropType<ValibotSchema | ValibotSchemaAsync>
38-
| PropType<ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any>>,
39+
| PropType<ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any>> | PropType<Struct<any, any>>,
3940
default: undefined
4041
},
4142
state: {
@@ -88,6 +89,8 @@ export default defineComponent({
8889
errs = errs.concat(await getJoiErrors(props.state, props.schema))
8990
} else if (isValibotSchema(props.schema)) {
9091
errs = errs.concat(await getValibotError(props.state, props.schema))
92+
} else if (isSuperStructSchema(props.schema)) {
93+
errs = errs.concat(await getSuperStructErrors(props.state, props.schema))
9194
} else {
9295
throw new Error('Form validation failed: Unsupported form schema')
9396
}
@@ -195,6 +198,15 @@ function isYupError (error: any): error is YupError {
195198
return error.inner !== undefined
196199
}
197200
201+
function isSuperStructSchema (schema: any): schema is Struct<any, any> {
202+
return (
203+
'schema' in schema &&
204+
typeof schema.coercer === 'function' &&
205+
typeof schema.validator === 'function' &&
206+
typeof schema.refiner === 'function'
207+
)
208+
}
209+
198210
async function getYupErrors (
199211
state: any,
200212
schema: YupObjectSchema<any>
@@ -218,6 +230,18 @@ function isZodSchema (schema: any): schema is ZodSchema {
218230
return schema.parse !== undefined
219231
}
220232
233+
async function getSuperStructErrors (state: any, schema: Struct<any, any>): Promise<FormError[]> {
234+
const [err] = schema.validate(state)
235+
if (err) {
236+
const errors = err.failures()
237+
return errors.map((error) => ({
238+
message: error.message,
239+
path: error.path.join('.')
240+
}))
241+
}
242+
return []
243+
}
244+
221245
async function getZodErrors (
222246
state: any,
223247
schema: ZodSchema
@@ -259,6 +283,7 @@ async function getJoiErrors (
259283
}
260284
}
261285
286+
262287
function isValibotSchema (schema: any): schema is ValibotSchema30 | ValibotSchemaAsync30 | ValibotSchema31 | ValibotSchemaAsync31 | ValibotSafeParser31<any, any> | ValibotSafeParserAsync31<any, any> | ValibotSchema | ValibotSchemaAsync | ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any> {
263288
return '_parse' in schema || '_run' in schema || (typeof schema === 'function' && 'schema' in schema)
264289
}

0 commit comments

Comments
 (0)
Please sign in to comment.