N/A

Validate app-owned form state and distribute errors to named fields.

Use your work email.

Tell us what you are building.

Installation

Add the shadcn-vue controls you want to compose, then add Stackhacker UI's form layer. form-field installs the Stackhacker UI field primitive and form dependency automatically.

pnpm dlx shadcn-vue@latest add input textarea select
pnpm dlx shadcn-vue@latest add "https://ui.stackhacker.io/r/form-field.json"

Usage

<script setup lang="ts">
import { reactive } from 'vue'
import { Form, type FormError } from '@/components/form'
import { FormField } from '@/components/form-field'
import { Input } from '@/components/ui/input'

const state = reactive({ email: '' })

function validate(values: typeof state): FormError[] {
  if (!values.email.includes('@')) {
    return [{ name: 'email', message: 'Enter a valid email address.' }]
  }
  return []
}

async function onSubmit(event) {
  await saveProfile(event.data)
}
</script>

<template>
  <Form :state="state" :validate="validate" @submit="onSubmit">
    <FormField v-slot="field" label="Email" name="email">
      <Input v-model="state.email" v-bind="field" type="email" />
    </FormField>
  </Form>
</template>

Responsibilities

Form owns the UI protocol: form event handling, validation triggering, error collection, field-level error distribution, and submit / error events.

Your app owns API submission, mutation, persistence, toast, redirect, server error interpretation, auth flows, and business rules.

Regle Integration

Regle remains an app dependency. Stackhacker UI does not import Regle.

<script setup lang="ts">
import { useRegle } from '@regle/core'
import { email, required, withMessage } from '@regle/rules'
import { Form } from '@/components/form'
import { FormField } from '@/components/form-field'
import { Input } from '@/components/ui/input'

const { r$ } = useRegle({ email: '' }, {
  email: {
    required,
    email: withMessage(email, 'Enter a valid email address.')
  }
})
</script>

<template>
  <Form :schema="r$" :state="r$.$value">
    <FormField v-slot="field" label="Email" name="email">
      <Input v-model="r$.$value.email" v-bind="field" type="email" />
    </FormField>
  </Form>
</template>

API Reference

Props

PropTypeDefaultDescription
stateunknownApp-owned reactive form state.
schemaunknownOptional schema-like validator such as Regle or Standard Schema.
validate(state) => FormError[] | Promise<FormError[]>Custom validation function.
validateOn('input' | 'change' | 'blur')[]['blur']Input events that trigger validation.
disabledbooleanfalseMarks the form context disabled for fields.
classstringAdditional CSS classes.

Events

EventPayloadDescription
submit{ originalEvent, data }Emitted after validation succeeds.
error{ originalEvent, errors, data }Emitted after validation fails.

Types

interface FormError {
  name: string
  message: string
  id?: string
}