Skip to content

Form ​

Experimental

A form component which allows users to input data.

vue
<script setup lang="ts">
import { z } from 'zod'
import {
  useHForm,
  HCheckbox,
  HButton,
} from '@holistics/design-system'
import EmailComposableField from './EmailComposableField.vue'
import PasswordComponentField from './PasswordComponentField.vue'

const schema = z.object({
  email: z.email(),
  password: z.string().min(8),
  rememberMe: z.boolean(),
})

const {
  handleSubmit, errors, values, HForm, HField,
} = useHForm({
  validationSchema: schema,
  initialValues: {
    email: '',
    password: '',
    rememberMe: false,
  },
})

const onSubmit = handleSubmit(data => {
  console.log(data)
})
</script>

<template>
  <div
    data-testid="container"
    class="flex max-w-md flex-col text-wrap"
  >
    <HForm>
      <EmailComposableField />
      <PasswordComponentField
        label="Password"
        name="password"
      />
      <HField
        v-slot="{ fieldProps }"
        name="rememberMe"
      >
        <HCheckbox
          v-bind="fieldProps"
          label="Remember Me"
        />
      </HField>
      <HButton
        type="primary-highlight"
        @click="onSubmit"
      >
        Submit
      </HButton>
    </HForm>
    <div class="mt-4">
      <pre>values: {{ values }}</pre>
      <pre>errors: {{ errors }}</pre>
    </div>
  </div>
</template>

Behaviors ​

Form overview

Necessity indicator ​

By default, if you define a field is required in the validation schema, the HField will automatically add a necessity indicator (a black asterisk) next to the label. You can customize this behavior by passing the necessity-indicator prop to the HField component.

html
<!-- To force hide the necessity indicator -->
<HField
  necessity-indicator="never"
/>

Validation states ​

The form exposes several reactive states via the meta object returned from useHForm:

  • valid: boolean - True if all fields are valid.
  • dirty: boolean - True if at least one field value has changed.
  • touched: boolean - True if at least one field has been blurred.
  • pending: boolean - True if validation is in progress (async validation).

Additionally, isSubmitting and isValidating refs are available directly from useHForm for tracking submission and validation status.

Examples ​

Validation mode ​

  • onBlur: Validates input after the field control is lost focus

  • onChange: Validates input in real-time after user interaction

vue
<script setup lang="ts">
import { z } from 'zod'
import { useHField, FormFieldValidationMode, HTextInput } from '@holistics/design-system'

const { HField } = useHField({
  path: 'email',
  rules: z.email(),
  opts: { validationMode: FormFieldValidationMode.OnBlur },
})

const { HField: HField2 } = useHField({
  path: 'email2',
  rules: z.email(),
  opts: { validationMode: FormFieldValidationMode.OnChange },
})
</script>

<template>
  <div
    class="flex flex-col gap-4"
    data-testid="container"
  >
    <HField label="Email (validate on blur)">
      <template #default="{ fieldProps }">
        <HTextInput
          v-bind="fieldProps"
          type="email"
          autocomplete="email"
        />
      </template>
    </HField>

    <HField2 label="Email (validate on change)">
      <template #default="{ fieldProps }">
        <HTextInput
          v-bind="fieldProps"
          type="email"
          autocomplete="email"
        />
      </template>
    </HField2>
  </div>
</template>

Label position ​

  • top: The label is placed above the field
vue
<script setup lang="ts">
import { z } from 'zod'
import { useHForm, HTextInput } from '@holistics/design-system'

const schema = z.object({
  name: z.string(),
  email: z.email(),
})

const { HForm, HField } = useHForm({
  validationSchema: schema,
})
</script>

<template>
  <HForm class="flex w-full max-w-md flex-col gap-4">
    <HField
      name="name"
      label="Name"
      label-position="top"
    >
      <template #default="{ fieldProps }">
        <HTextInput
          v-bind="fieldProps"
          placeholder="Enter your name"
        />
      </template>
    </HField>

    <HField
      name="email"
      label="Email"
      label-position="top"
    >
      <template #default="{ fieldProps }">
        <HTextInput
          v-bind="fieldProps"
          placeholder="Enter your email"
        />
      </template>
    </HField>
  </HForm>
</template>
  • left: The label is placed to the left of the field
vue
<script setup lang="ts">
import { z } from 'zod'
import { useHForm, HTextInput } from '@holistics/design-system'

const schema = z.object({
  name: z.string(),
  email: z.email(),
})

const { HForm, HField } = useHForm({
  validationSchema: schema,
})
</script>

<template>
  <HForm class="flex w-full max-w-md flex-col gap-4">
    <HField
      name="name"
      label="Name"
      label-position="left"
    >
      <template #default="{ fieldProps }">
        <HTextInput
          v-bind="fieldProps"
          placeholder="Enter your name"
        />
      </template>
    </HField>

    <HField
      name="email"
      label="Email"
      label-position="left"
    >
      <template #default="{ fieldProps }">
        <HTextInput
          v-bind="fieldProps"
          placeholder="Enter your email"
        />
      </template>
    </HField>
  </HForm>
</template>

Integrate with field controls ​

Example with Select, Radio Group and Checkbox

vue
<script setup lang="ts">
import { z } from 'zod'
import type { FormFieldLabelPosition } from '@holistics/design-system'
import {
  useHForm,
  HTextInput,
  HSelect,
  HRadioGroup,
  HRadio,
  HCheckbox,
  HButton,
} from '@holistics/design-system'

const props = defineProps<{
  labelPosition: FormFieldLabelPosition
}>()

const {
  HForm, HField, values, handleSubmit, handleReset,
} = useHForm({
  validationSchema: z.object({
    databaseType: z.enum(['mysql', 'postgresql']),
    name: z.string().min(1),
    connectionMode: z.enum(['direct', 'proxy']),
    host: z.string().min(1),
    port: z.number().min(1),
    username: z.string().min(1),
    password: z.string().min(1),
    database: z.string().min(1),
    requireSSL: z.boolean(),
  }),
  initialValues: {
    connectionMode: 'direct',
  },
})

const onSubmit = handleSubmit(data => {
  console.log(data)
})
</script>

<template>
  <div class="w-2/3 rounded border p-3">
    <HForm
      :label-position="props.labelPosition"
    >
      <HField
        v-slot="{ fieldProps: { id, ...rest } }"
        label="Database type"
        name="databaseType"
      >
        <HSelect
          v-bind="rest"
          :input-id="id"
          filterable
          :options="[
            {
              label: 'MySQL',
              value: 'mysql',
            },
            {
              label: 'PostgreSQL',
              value: 'postgresql',
            },
          ]"
        />
      </HField>
      <HField
        v-slot="{ fieldProps }"
        label="Display name"
        name="name"
        :tooltip="{
          content: 'The name of the data source',
        }"
      >
        <HTextInput
          v-bind="fieldProps"
        />
      </HField>
      <HField
        v-slot="{ fieldProps }"
        label="Connection mode"
        name="connectionMode"
        necessary-indicator="never"
      >
        <HRadioGroup
          v-bind="fieldProps"
          orientation="horizontal"
          class="flex gap-4"
        >
          <HRadio
            value="direct"
          >
            Direct connection
          </HRadio>
          <HRadio
            value="proxy"
          >
            Use reverse tunnel
          </HRadio>
        </HRadioGroup>
      </HField>
      <hr class="my-4">
      <div class="grid grid-cols-2 gap-4">
        <HField
          v-slot="{ fieldProps }"
          label="Host"
          name="host"
          :tooltip="{
            content: 'The host of the data source',
          }"
        >
          <HTextInput
            v-bind="fieldProps"
          />
        </HField>
        <HField
          v-slot="{ fieldProps }"
          label="HTTP(S) Port"
          name="port"
          :tooltip="{
            content: 'The port of the data source',
          }"
        >
          <HTextInput
            v-bind="fieldProps"
            type="number"
          />
        </HField>
      </div>
      <HField
        v-slot="{ fieldProps }"
        label="Database name"
        name="database"
      >
        <HTextInput
          v-bind="fieldProps"
        />
      </HField>
      <div class="grid grid-cols-2 gap-4">
        <HField
          v-slot="{ fieldProps }"
          label="Username"
          name="username"
        >
          <HTextInput
            v-bind="fieldProps"
          />
        </HField>
        <HField
          v-slot="{ fieldProps }"
          label="Password"
          name="password"
        >
          <HTextInput
            v-bind="fieldProps"
          />
        </HField>
      </div>
      <HField
        v-slot="{ fieldProps }"
        name="requireSSL"
      >
        <HCheckbox
          v-bind="fieldProps"
          label="Require SSL"
        />
      </HField>
      <div class="flex gap-2">
        <HButton
          type="primary-highlight"
          @click="onSubmit"
        >
          Test connection
        </HButton>
        <HButton
          type="secondary-default"
          @click="handleReset"
        >
          Reset
        </HButton>
      </div>
    </HForm>
    <pre>Values: {{ values }}</pre>
  </div>
</template>

API ​

Pass-through: <div> ​

What does this mean?

All props, events, and attrs that are not specified in the tables below will be passed to the element/component described above.

Props ​

NameTypeDescription
labelPosition 
FormFieldLabelPosition
= "top"
labelAlign 
FormFieldLabelAlign
= "left"
disabled 
boolean
= false
spacing 
"sm" | "md" | "lg"
= "md"

Spacing between fields

Slots ​

NameScopedDescription
#default
{}

useHForm composable ​

Parameters ​

NameTypeDescription
validationSchema *ZodType<object, object>Zod schema definition that powers both validation and the slot typings of the form.
initialValues PartialDeep<Input> | null | undefinedOptional initial values for the schema keys. Useful for editing flows or seeding defaults.

Return ​

NameTypeDescription
HForm typeof HFormHolistics form wrapper component that wires labels, layout spacing, and submission context.
HField typeof HFieldTyped field component that maps slot bindings to the schema fields defined in the composable.
HFieldSubscribe typeof HFieldSubscribeLight-weight subscription component that lets you react to field-level state without rendering controls.
values GenericObjectReactive object that always reflects the current raw form values.
errors ComputedRef<Record<string, string | undefined>>Computed error bag keyed by field path.
meta ComputedRef<FormMeta<GenericObject>>Aggregated metadata such as dirty, touched, and valid flags sourced from vee-validate.
handleSubmit (<TReturn>(cb: SubmissionHandler) => (event?: Event) => Promise<TReturn | undefined>)Utility that validates the form and only invokes the provided callback when the data passes.
handleReset () => voidResets every field back to its initial value and clears touched/error state.
submitForm (event?: unknown) => Promise<void>Promise-based submit helper that mirrors native form submission semantics.
resetField (path: string, state?: Partial<FieldState>) => voidResets an individual field to a known state, optionally overriding parts of its meta.
validate (options?: Partial<ValidationOptions>) => Promise<FormValidationResult>Runs validation for the entire form and resolves with the aggregated result.
validateField (path: string, options?: Partial<ValidationOptions>) => Promise<ValidationResult>Validates a single field path and returns its validation outcome.
setFieldValue (path: string, value: unknown, shouldValidate?: boolean) => voidProgrammatically updates a field value and optionally re-validates it.
setValues (fields: Record<string, unknown>) => voidBulk-updates multiple field values at once, useful for patching server data.
setErrors (errors: Record<string, string | string[] | undefined>) => voidAllows manually setting validation errors, for example when the server returns API issues.
setTouched (fields: boolean | Record<string, boolean>) => voidMarks one or many fields as touched or untouched.
defineComponentBinds <TPath extends string>(path: TPath, options?: Record<string, unknown>) => Record<string, unknown>Helper that returns binding objects so you can spread form props directly onto inputs.
controlledValues Ref<GenericObject>Reactive reference that always mirrors the values being tracked internally by vee-validate.
submitCount Ref<number>Counts how many times the form attempted to submit.
errorBag Ref<Partial<Record<string, string[]>>>Holds the raw multi-message errors emitted by vee-validate for each field path.
isSubmitting Ref<boolean>Flags whether a submit handler is still pending.
isValidating Ref<boolean>Signals that synchronous or asynchronous validation is currently running.
createPathState (path: MaybeRef<string>, config?: Partial<PathStateConfig>) => PathStateCreates a derived object that tracks a single field path, handy for watchers or custom UIs.
name stringThe unique name vee-validate assigns to this form instance.