Form
Usage
Use the Form component to validate form data using any validation library supporting Standard Schema such as Valibot, Zod, Regle, Yup, Joi or Superstruct or your own validation logic.
It works with the FormField 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- any Standard Schema or Superstruct.
<script setup lang="ts">
import type { FormSubmitEvent } from 'pohon-ui';
import * as v from 'valibot';
const schema = v.object({
email: v.pipe(v.string(), v.email('Invalid email')),
password: v.pipe(v.string(), v.minLength(8, 'Must be at least 8 characters')),
});
type Schema = v.InferOutput<typeof schema>;
const state = reactive({
email: '',
password: '',
});
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
:schema="schema"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Password"
name="password"
>
<PInput
v-model="state.password"
type="password"
/>
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import type { FormSubmitEvent } from 'pohon-ui';
import * as v from 'valibot';
const schema = v.object({
email: v.pipe(v.string(), v.email('Invalid email')),
password: v.pipe(v.string(), v.minLength(8, 'Must be at least 8 characters')),
});
type Schema = v.InferOutput<typeof schema>;
const state = reactive({
email: '',
password: '',
});
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
:schema="schema"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Password"
name="password"
>
<PInput
v-model="state.password"
type="password"
/>
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
<script setup lang="ts">
import type { FormSubmitEvent } from 'pohon-ui';
import * as z from 'zod';
const schema = z.object({
email: z.email('Invalid email'),
password: z.string('Password is required').min(8, 'Must be at least 8 characters'),
});
type Schema = z.output<typeof schema>;
const state = reactive<Partial<Schema>>({
email: undefined,
password: undefined,
});
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
:schema="schema"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Password"
name="password"
>
<PInput
v-model="state.password"
type="password"
/>
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import type { FormSubmitEvent } from 'pohon-ui';
import * as z from 'zod';
const schema = z.object({
email: z.email('Invalid email'),
password: z.string('Password is required').min(8, 'Must be at least 8 characters'),
});
type Schema = z.output<typeof schema>;
const state = reactive<Partial<Schema>>({
email: undefined,
password: undefined,
});
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
:schema="schema"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Password"
name="password"
>
<PInput
v-model="state.password"
type="password"
/>
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
<script setup lang="ts">
import type { InferInput } from '@regle/core';
import type { FormSubmitEvent } from 'pohon-ui';
import { useRegle } from '@regle/core';
import { email, minLength, required, withMessage } from '@regle/rules';
const { r$ } = useRegle({ email: '', password: '' }, {
email: { required, email: withMessage(email, 'Invalid email') },
password: { required, minLength: withMessage(minLength(8), 'Must be at least 8 characters') },
});
type Schema = InferInput<typeof r$>;
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
:schema="r$"
:state="r$.$value"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="r$.$value.email" />
</PFormField>
<PFormField
label="Password"
name="password"
>
<PInput
v-model="r$.$value.password"
type="password"
/>
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
<script setup lang="ts">
import type { FormSubmitEvent } from 'pohon-ui';
import type { InferType } from 'yup';
import { object, string } from 'yup';
const schema = object({
email: string().email('Invalid email').required('Required'),
password: string()
.min(8, 'Must be at least 8 characters')
.required('Required'),
});
type Schema = InferType<typeof schema>;
const state = reactive({
email: undefined,
password: undefined,
});
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
:schema="schema"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Password"
name="password"
>
<PInput
v-model="state.password"
type="password"
/>
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import type { FormSubmitEvent } from 'pohon-ui';
import type { InferType } from 'yup';
import { object, string } from 'yup';
const schema = object({
email: string().email('Invalid email').required('Required'),
password: string()
.min(8, 'Must be at least 8 characters')
.required('Required'),
});
type Schema = InferType<typeof schema>;
const state = reactive({
email: undefined,
password: undefined,
});
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
:schema="schema"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Password"
name="password"
>
<PInput
v-model="state.password"
type="password"
/>
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
<script setup lang="ts">
import type { FormSubmitEvent } from 'pohon-ui';
import Joi from 'joi';
const schema = Joi.object({
email: Joi.string().required(),
password: Joi.string()
.min(8)
.required(),
});
const state = reactive({
email: undefined,
password: undefined,
});
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<typeof state>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
:schema="schema"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Password"
name="password"
>
<PInput
v-model="state.password"
type="password"
/>
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import type { FormSubmitEvent } from 'pohon-ui';
import Joi from 'joi';
const schema = Joi.object({
email: Joi.string().required(),
password: Joi.string()
.min(8)
.required(),
});
const state = reactive({
email: undefined,
password: undefined,
});
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<typeof state>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
:schema="schema"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Password"
name="password"
>
<PInput
v-model="state.password"
type="password"
/>
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
<script setup lang="ts">
import type { FormSubmitEvent } from 'pohon-ui';
import type { Infer } from 'superstruct';
import { nonempty, object, refine, string } from 'superstruct';
const schema = object({
email: nonempty(string()),
password: refine(string(), 'Password', (value) => {
if (value.length >= 8) {
return true;
}
return 'Must be at least 8 characters';
}),
});
const state = reactive({
email: '',
password: '',
});
type Schema = Infer<typeof schema>;
async function onSubmit(event: FormSubmitEvent<Schema>) {
console.log(event.data);
}
</script>
<template>
<PForm
:schema="schema"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Password"
name="password"
>
<PInput
v-model="state.password"
type="password"
/>
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import type { FormSubmitEvent } from 'pohon-ui';
import type { Infer } from 'superstruct';
import { nonempty, object, refine, string } from 'superstruct';
const schema = object({
email: nonempty(string()),
password: refine(string(), 'Password', (value) => {
if (value.length >= 8) {
return true;
}
return 'Must be at least 8 characters';
}),
});
const state = reactive({
email: '',
password: '',
});
type Schema = Infer<typeof schema>;
async function onSubmit(event: FormSubmitEvent<Schema>) {
console.log(event.data);
}
</script>
<template>
<PForm
:schema="schema"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Password"
name="password"
>
<PInput
v-model="state.password"
type="password"
/>
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
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- thenameof theFormFieldto send the error to.
schema prop to handle complex use cases.<script setup lang="ts">
import type { FormError, FormSubmitEvent } from 'pohon-ui';
const state = reactive({
email: undefined,
password: undefined,
});
type Schema = typeof state;
function validate(state: Partial<Schema>): Array<FormError> {
const errors = [];
if (!state.email) {
errors.push({ name: 'email', message: 'Required' });
}
if (!state.password) {
errors.push({ name: 'password', message: 'Required' });
}
return errors;
}
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
:validate="validate"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Password"
name="password"
>
<PInput
v-model="state.password"
type="password"
/>
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import type { FormError, FormSubmitEvent } from 'pohon-ui';
const state = reactive({
email: undefined,
password: undefined,
});
type Schema = typeof state;
function validate(state: Partial<Schema>): Array<FormError> {
const errors = [];
if (!state.email) {
errors.push({ name: 'email', message: 'Required' });
}
if (!state.password) {
errors.push({ name: 'password', message: 'Required' });
}
return errors;
}
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
:validate="validate"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Password"
name="password"
>
<PInput
v-model="state.password"
type="password"
/>
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
Error reporting
Errors are matched to the corresponding FormField using its name prop. An error on the email field is shown by <FormField name="email">.
Nested fields are matched using dot notation. A schema like { user: z.object({ email: z.string() }) } will be applied to <FormField name="user.email">.
tags.0, tags.1) and won't match <FormField name="tags"> by name alone. Use the error-pattern prop with a regular expression like /^tags\..+/ to capture them. This is especially useful for components like InputTags.<script setup lang="ts">
import type { FormSubmitEvent } from 'pohon-ui';
import * as z from 'zod';
const schema = z.object({
email: z.email('Invalid email'),
tags: z.array(z.string().regex(/^[a-z-]+$/, 'Lowercase letters and dashes only')).min(1, 'Please add at least one tag'),
});
type Schema = z.output<typeof schema>;
const state = reactive<Partial<Schema>>({
email: undefined,
tags: [],
});
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
:schema="schema"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Tags"
name="tags"
:error-pattern="/^tags\..+/"
>
<PInputTags v-model="state.tags" />
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import type { FormSubmitEvent } from 'pohon-ui';
import * as z from 'zod';
const schema = z.object({
email: z.email('Invalid email'),
tags: z.array(z.string().regex(/^[a-z-]+$/, 'Lowercase letters and dashes only')).min(1, 'Please add at least one tag'),
});
type Schema = z.output<typeof schema>;
const state = reactive<Partial<Schema>>({
email: undefined,
tags: [],
});
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
:schema="schema"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Tags"
name="tags"
:error-pattern="/^tags\..+/"
>
<PInputTags v-model="state.tags" />
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
Input events
The Form component automatically triggers validation when an input emits an input, change, or blur event.
- Validation on
inputoccurs as you type. - Validation on
changeoccurs when you commit to a value. - Validation on
blurhappens when an input loses focus.
You can control when validation happens this using the validate-on prop.
useFormField 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'sid.name- thenameof theFormFieldmessage- the error message to display.
Here's an example that focuses the first input element with an error after the form is submitted:
<script setup lang="ts">
import type { FormError, FormErrorEvent, FormSubmitEvent } from 'pohon-ui';
const state = reactive({
email: undefined,
password: undefined,
});
type Schema = typeof state;
function validate(state: Partial<Schema>): Array<FormError> {
const errors = [];
if (!state.email) {
errors.push({ name: 'email', message: 'Required' });
}
if (!state.password) {
errors.push({ name: 'password', message: 'Required' });
}
return errors;
}
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
async function onError(event: FormErrorEvent) {
if (event?.errors?.[0]?.id) {
const element = document.getElementById(event.errors[0].id);
element?.focus();
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
</script>
<template>
<PForm
:validate="validate"
:state="state"
class="space-y-4"
@submit="onSubmit"
@error="onError"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Password"
name="password"
>
<PInput
v-model="state.password"
type="password"
/>
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import type { FormError, FormErrorEvent, FormSubmitEvent } from 'pohon-ui';
const state = reactive({
email: undefined,
password: undefined,
});
type Schema = typeof state;
function validate(state: Partial<Schema>): Array<FormError> {
const errors = [];
if (!state.email) {
errors.push({ name: 'email', message: 'Required' });
}
if (!state.password) {
errors.push({ name: 'password', message: 'Required' });
}
return errors;
}
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
async function onError(event: FormErrorEvent) {
if (event?.errors?.[0]?.id) {
const element = document.getElementById(event.errors[0].id);
element?.focus();
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
</script>
<template>
<PForm
:validate="validate"
:state="state"
class="space-y-4"
@submit="onSubmit"
@error="onError"
>
<PFormField
label="Email"
name="email"
>
<PInput v-model="state.email" />
</PFormField>
<PFormField
label="Password"
name="password"
>
<PInput
v-model="state.password"
type="password"
/>
</PFormField>
<PButton type="submit">
Submit
</PButton>
</PForm>
</template>
HTML5 validation
When calling form.submit() programmatically, the Form component automatically triggers native HTML5 validation before submission.
<script setup lang="ts">
import type { FormSubmitEvent } from 'pohon-ui';
const state = reactive({
email: undefined,
age: undefined,
});
type Schema = typeof state;
const form = useTemplateRef('form');
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<div class="space-y-4">
<PForm
ref="form"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput
v-model="state.email"
type="email"
required
/>
</PFormField>
<PFormField
label="Age"
name="age"
>
<PInput
v-model="state.age"
type="number"
min="18"
max="100"
required
/>
</PFormField>
</PForm>
<PButton @click="form?.submit()">
Submit
</PButton>
</div>
</template>
<script setup lang="ts">
import { reactive, useTemplateRef } from 'vue'
import type { FormSubmitEvent } from 'pohon-ui';
const state = reactive({
email: undefined,
age: undefined,
});
type Schema = typeof state;
const form = useTemplateRef('form');
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<div class="space-y-4">
<PForm
ref="form"
:state="state"
class="space-y-4"
@submit="onSubmit"
>
<PFormField
label="Email"
name="email"
>
<PInput
v-model="state.email"
type="email"
required
/>
</PFormField>
<PFormField
label="Age"
name="age"
>
<PInput
v-model="state.age"
type="number"
min="18"
max="100"
required
/>
</PFormField>
</PForm>
<PButton @click="form?.submit()">
Submit
</PButton>
</div>
</template>
Nesting forms
Use the nested prop to nest multiple Form components and link their validation functions. In this case, validating the parent form will automatically validate all the other forms inside it.
Nested forms directly inherit their parent's state, so you don't need to define a separate state for them. You can use the name prop to target a nested attribute within the parent's state.
It can be used to dynamically add fields based on user's input:
<script setup lang="ts">
import type { FormSubmitEvent } from 'pohon-ui';
import * as z from 'zod';
const schema = z.object({
name: z.string().min(2),
news: z.boolean().default(false),
});
type Schema = z.output<typeof schema>;
const nestedSchema = z.object({
email: z.email(),
});
type NestedSchema = z.output<typeof nestedSchema>;
const state = reactive<Partial<Schema & NestedSchema>>({ });
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
ref="form"
:state="state"
:schema="schema"
class="flex flex-col gap-4 w-60"
@submit="onSubmit"
>
<PFormField
label="Name"
name="name"
>
<PInput
v-model="state.name"
placeholder="John Lennon"
/>
</PFormField>
<div>
<PCheckbox
v-model="state.news"
name="news"
label="Register to our newsletter"
@update:model-value="state.email = undefined"
/>
</div>
<PForm
v-if="state.news"
:schema="nestedSchema"
nested
>
<PFormField
label="Email"
name="email"
>
<PInput
v-model="state.email"
placeholder="john@lennon.com"
/>
</PFormField>
</PForm>
<div>
<PButton type="submit">
Submit
</PButton>
</div>
</PForm>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import type { FormSubmitEvent } from 'pohon-ui';
import * as z from 'zod';
const schema = z.object({
name: z.string().min(2),
news: z.boolean().default(false),
});
type Schema = z.output<typeof schema>;
const nestedSchema = z.object({
email: z.email(),
});
type NestedSchema = z.output<typeof nestedSchema>;
const state = reactive<Partial<Schema & NestedSchema>>({ });
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
ref="form"
:state="state"
:schema="schema"
class="flex flex-col gap-4 w-60"
@submit="onSubmit"
>
<PFormField
label="Name"
name="name"
>
<PInput
v-model="state.name"
placeholder="John Lennon"
/>
</PFormField>
<div>
<PCheckbox
v-model="state.news"
name="news"
label="Register to our newsletter"
@update:model-value="state.email = undefined"
/>
</div>
<PForm
v-if="state.news"
:schema="nestedSchema"
nested
>
<PFormField
label="Email"
name="email"
>
<PInput
v-model="state.email"
placeholder="john@lennon.com"
/>
</PFormField>
</PForm>
<div>
<PButton type="submit">
Submit
</PButton>
</div>
</PForm>
</template>
Or to validate list inputs:
<script setup lang="ts">
import type { FormSubmitEvent } from 'pohon-ui';
import * as z from 'zod';
const schema = z.object({
customer: z.string().min(2),
});
type Schema = z.output<typeof schema>;
const itemSchema = z.object({
description: z.string().min(1),
price: z.number().min(0),
});
type ItemSchema = z.output<typeof itemSchema>;
const state = reactive<Partial<Schema & { items: Array<Partial<ItemSchema>> }>>({ });
function addItem() {
if (!state.items) {
state.items = [];
}
state.items.push({});
}
function removeItem() {
if (state.items) {
state.items.pop();
}
}
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
:state="state"
:schema="schema"
class="flex flex-col gap-4 w-60"
@submit="onSubmit"
>
<PFormField
label="Customer"
name="customer"
>
<PInput
v-model="state.customer"
placeholder="Wonka Industries"
/>
</PFormField>
<PForm
v-for="item, count in state.items"
:key="count"
:name="`items.${count}`"
:schema="itemSchema"
class="flex gap-2"
nested
>
<PFormField
:label="!count ? 'Description' : undefined"
name="description"
>
<PInput v-model="item.description" />
</PFormField>
<PFormField
:label="!count ? 'Price' : undefined"
name="price"
class="w-20"
>
<PInput
v-model="item.price"
type="number"
/>
</PFormField>
</PForm>
<div class="flex gap-2">
<PButton
color="neutral"
variant="subtle"
size="sm"
@click="addItem()"
>
Add Item
</PButton>
<PButton
color="neutral"
variant="ghost"
size="sm"
@click="removeItem()"
>
Remove Item
</PButton>
</div>
<div>
<PButton type="submit">
Submit
</PButton>
</div>
</PForm>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import type { FormSubmitEvent } from 'pohon-ui';
import * as z from 'zod';
const schema = z.object({
customer: z.string().min(2),
});
type Schema = z.output<typeof schema>;
const itemSchema = z.object({
description: z.string().min(1),
price: z.number().min(0),
});
type ItemSchema = z.output<typeof itemSchema>;
const state = reactive<Partial<Schema & { items: Array<Partial<ItemSchema>> }>>({ });
function addItem() {
if (!state.items) {
state.items = [];
}
state.items.push({});
}
function removeItem() {
if (state.items) {
state.items.pop();
}
}
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' });
console.log(event.data);
}
</script>
<template>
<PForm
:state="state"
:schema="schema"
class="flex flex-col gap-4 w-60"
@submit="onSubmit"
>
<PFormField
label="Customer"
name="customer"
>
<PInput
v-model="state.customer"
placeholder="Wonka Industries"
/>
</PFormField>
<PForm
v-for="item, count in state.items"
:key="count"
:name="`items.${count}`"
:schema="itemSchema"
class="flex gap-2"
nested
>
<PFormField
:label="!count ? 'Description' : undefined"
name="description"
>
<PInput v-model="item.description" />
</PFormField>
<PFormField
:label="!count ? 'Price' : undefined"
name="price"
class="w-20"
>
<PInput
v-model="item.price"
type="number"
/>
</PFormField>
</PForm>
<div class="flex gap-2">
<PButton
color="neutral"
variant="subtle"
size="sm"
@click="addItem()"
>
Add Item
</PButton>
<PButton
color="neutral"
variant="ghost"
size="sm"
@click="removeItem()"
>
Remove Item
</PButton>
</div>
<div>
<PButton type="submit">
Submit
</PButton>
</div>
</PForm>
</template>
API
Props
| Prop | Default | Type |
|---|
Slots
| Slot | Type |
|---|
Emits
| Event | Type |
|---|
Expose
You can access the typed component instance using useTemplateRef.
<script setup lang="ts">
const form = useTemplateRef('form');
</script>
<template>
<PForm ref="form" />
</template>
This will give you access to the following:
| Name | Type |
|---|---|
submit() | Promise<void> Triggers form submission with HTML5 validation. |
validate(opts: { name?: keyof T | (keyof T)[], silent?: boolean, nested?: boolean, transform?: boolean }) | Promise<T> Triggers form validation. Will raise any errors unless |
clear(path?: keyof T | RegExp) | void Clears form errors associated with a specific path. If no path is provided, clears all form errors. |
getErrors(path?: keyof T | RegExp) | FormErrorWithId[] Retrieves form errors associated with a specific path. If no path is provided, returns all form errors. |
setErrors(errors: FormError[], name?: keyof T | RegExp) | void Sets form errors for a given path. If no path is provided, overrides all errors. |
errors | Ref<FormErrorWithId[]> A reference to the array containing validation errors. Use this to access or manipulate the error information. |
disabled | Ref<boolean> |
dirty | Ref<boolean> true if at least one form field has been updated by the user. |
dirtyFields | ReadonlySet<DeepReadonly<keyof T>> Tracks fields that have been modified by the user. |
touchedFields | ReadonlySet<DeepReadonly<keyof T>> Tracks fields that the user interacted with. |
blurredFields | ReadonlySet<DeepReadonly<keyof T>> Tracks fields blurred by the user. |
Theme
export default defineAppConfig({
ui: {
form: {
base: ''
}
}
})import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from 'pohon-ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
form: {
base: ''
}
}
})
]
})