Forms ​
A guide to building forms with Holistics components.
Form mechanism ​
We use the vee-validate package for the form logic and zod package for the validation schema. The form and field communicate using the provide/inject pattern.
List of supported field controls:
Create your first form ​
Initialize the form schema and validation ​
- Define the validation schema with
zod:
import { z } from 'zod'
const schema = z.object({
email: z.email(),
password: z.string().min(8),
rememberMe: z.boolean(),
})- Init the form with
useHFormcomposable:
const {
handleSubmit,
HForm,
HField,
} = useHForm({
validationSchema: schema,
initialValues: {
email: '',
password: '',
rememberMe: false,
},
})Integrate components ​
Use HForm and HField from the composable to design the form. The HField component exposes a fieldProps object in its default slot, which can be bound directly to input components:
<HForm
label-position="top"
>
<HField
v-slot="{ fieldProps }"
label="Then"
name="then"
>
<HTextInput
v-bind="fieldProps"
placeholder="Input value..."
/>
</HField>
</HForm>Alternatively, you can manually bind the props if you need more control or are using custom components that don't follow the standard prop naming:
<HField
v-slot="{ id, value, errors }"
label="Then"
name="then"
>
<input
:id="id"
v-model="value.value"
:aria-invalid="!!errors.value.length"
placeholder="Input value..."
class="border p-2 rounded"
/>
</HField>Submitting data ​
Use handleSubmit to wrap your submission logic. This function ensures validation runs before your callback is executed.
const onSubmit = handleSubmit(data => {
// The data is validated here
console.log(data)
})<HButton
type="primary-highlight"
@click="onSubmit"
>
Submit
</HButton>Form with nested components ​
In a large form, you may need to break it down into multiple components. In this case, you can use the useHField composable in child components to inject the field logic into the root form.
1. In the parent or root form, define the final schema and initial values:
<!-- ParentForm.vue -->
<script setup lang="ts">
import { useHForm } from '@holistics/design-system'
import { z } from 'zod'
import ChildComponent from './ChildComponent.vue'
const schema = z.object({
name: z.string().min(1),
})
const { HForm } = useHForm({ validationSchema: schema })
</script>
<template>
<HForm>
<ChildComponent />
</HForm>
</template>2. Then, in the child component, you can either:
a. Use useHField composable with the field name:
<!-- ChildComponent.vue -->
<script setup lang="ts">
import { useHField, HTextInput } from '@holistics/design-system'
const { value: nameValue } = useHField<string>({
path: 'name', // Must match the key in the parent schema
})
</script>
<template>
<HTextInput
v-model="nameValue"
label="Name"
/>
</template>b. Import and use HField directly (if extracted from useHForm somewhere or generic HField)
<!-- ChildComponent.vue -->
<script setup lang="ts">
import { HField, HTextInput } from '@holistics/design-system'
</script>
<template>
<HField
v-slot="{ fieldProps }"
label="Name"
>
<HTextInput v-bind="fieldProps" />
</HField>
</template>Validation and error ​
Validation is handled automatically by vee-validate based on the zod schema.
The submit button should remain enabled, as handleSubmit will prevent submission if the form is invalid.
Standalone and reusable field ​
If you want to display a field or use the field validation without a form, you can use the useHField composable standalone.
<script setup lang="ts">
import { z } from 'zod'
import { useHField, HTextInput } from '@holistics/design-system'
const { value: emailValue, HField } = useHField<string>({
path: 'email',
rules: z.email(),
opts: { initialValue: '[email protected]' },
})
</script>
<template>
<HField
label="Email"
label-position="top"
>
<template #default="{ fieldProps }">
<HTextInput
v-bind="fieldProps"
placeholder="Enter your email"
/>
</template>
</HField>
</template>Later, you can reuse this field logic inside any parent form.