Components
Form
The form element itself: native submit interception handing the event to the engine, one grid, one spacing voice.
#Basic usage
<script setup lang="ts">
import { Button, Form, FormField, Input, Textarea, useForm } from "@bysages/vue";
import { ref } from "vue";
import { z } from "zod";
const status = ref("");
const form = useForm({
defaultValues: { title: "", abstract: "" },
validators: {
onChange: z.object({
title: z.string().min(1, "The title is required."),
abstract: z.string().min(8, "Write at least 8 characters."),
}),
},
onSubmit: () => {
status.value = "Submitted.";
},
});
</script>
<template>
<Form :form="form" class="w-full">
<FormField name="title" label="Title" hint="One line, no period" required>
<template #default="{ field }">
<Input
:model-value="field.state.value"
placeholder="Title of the piece"
@update:model-value="field.handleChange"
@blur="field.handleBlur"
/>
</template>
</FormField>
<FormField name="abstract" label="Abstract">
<template #default="{ field }">
<Textarea
:model-value="field.state.value"
placeholder="What the piece says"
@update:model-value="field.handleChange"
@blur="field.handleBlur"
/>
</template>
</FormField>
<Button type="submit">Submit</Button>
</Form>
<p role="status" class="mt-3 text-sm text-tertiary">
{{ status }}
</p>
</template>
<script setup lang="ts">
import { Button, Form, FormField, Input, Textarea, useForm } from "@bysages/vue";
import { ref } from "vue";
const status = ref("");
const form = useForm({
defaultValues: { title: "", abstract: "" },
onSubmit: () => {
status.value = "Submitted.";
},
});
const titleValidators = {
onChange: ({ value }: { value: string }) => (value ? undefined : "Title is required"),
};
const abstractValidators = {
onChange: ({ value }: { value: string }) => {
if (!value) return "The abstract is required";
if (value.length < 8) return "At least 8 characters";
return undefined;
},
};
</script>
<template>
<Form :form="form" class="w-full">
<FormField
name="title"
label="Title"
hint="One line, no period"
required
:validators="titleValidators"
>
<template #default="{ field }">
<Input
:model-value="field.state.value"
placeholder="Title of the piece"
@update:model-value="field.handleChange"
@blur="field.handleBlur"
/>
</template>
</FormField>
<FormField name="abstract" label="Abstract" :validators="abstractValidators">
<template #default="{ field }">
<Textarea
:model-value="field.state.value"
placeholder="What the piece says"
@update:model-value="field.handleChange"
@blur="field.handleBlur"
/>
</template>
</FormField>
<Button type="submit">Submit</Button>
</Form>
<p role="status" class="mt-3 text-sm text-tertiary">
{{ status }}
</p>
</template>
<script setup lang="ts">
import {
Button,
CheckboxGroup,
Form,
FormField,
Input,
Switch,
Textarea,
useForm,
} from "@bysages/vue";
import { ref } from "vue";
import { z } from "zod";
const status = ref("");
const topics = [
{ label: "Typography", value: "typography" },
{ label: "Lighting", value: "lighting" },
{ label: "Motion", value: "motion" },
];
const form = useForm({
defaultValues: {
title: "",
summary: "",
topics: [] as string[],
consent: false,
},
validators: {
onChange: z.object({
title: z.string().min(1, "The title is required."),
summary: z.string().min(8, "Write at least 8 characters."),
topics: z.array(z.string()).min(1, "Pick at least one topic."),
consent: z.boolean().refine((v) => v, "Please accept the terms."),
}),
},
onSubmit: () => {
status.value = "Submitted.";
},
});
</script>
<template>
<Form :form="form" class="w-full">
<FormField name="title" label="Title" required>
<template #default="{ field }">
<Input
:model-value="field.state.value"
placeholder="Title of the piece"
@update:model-value="field.handleChange"
@blur="field.handleBlur"
/>
</template>
</FormField>
<FormField name="summary" label="Summary" hint="A few sentences">
<template #default="{ field }">
<Textarea
:model-value="field.state.value"
placeholder="What the piece says"
@update:model-value="field.handleChange"
@blur="field.handleBlur"
/>
</template>
</FormField>
<FormField name="topics" label="Topics" required>
<template #default="{ field }">
<CheckboxGroup
:model-value="field.state.value"
:options="topics"
@update:model-value="field.handleChange"
@blur="field.handleBlur"
/>
</template>
</FormField>
<FormField name="consent">
<template #default="{ field }">
<Switch.Root
:checked="field.state.value"
@update:checked="field.handleChange"
@blur="field.handleBlur"
>
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
<Switch.Label>I accept the terms</Switch.Label>
<Switch.HiddenInput />
</Switch.Root>
</template>
</FormField>
<Button type="submit">Submit</Button>
</Form>
<p role="status" class="mt-3 text-sm text-tertiary">
{{ status }}
</p>
</template>
#Props
#Form
The form element itself: native submit interception handing the event to the engine, one grid, one spacing voice.
| Prop | Type | Default | Description |
|---|---|---|---|
form | Object as PropType<AnyFormApi> | undefined | The engine instance from useForm — values, validation and submit all live there. Optional: a form assembled without an engine (the generative vessel) degrades to layout and native semantics. |
#FormField
The named slot in the grid: label, control, hint — and the engine's errors for this name, shown through the same parts the standalone Field family styles. The default slot receives the engine's live field (value, handleChange, handleBlur, full state), so every capability it has is right there where the control is wired.
| Prop | Type | Default | Description |
|---|---|---|---|
form | Object as PropType<AnyFormApi> | undefined | Overrides the enclosing Form's engine — for a field assembled on its own. |
name | String | The engine's key for this slot's value and errors. | |
label | String | undefined | The heading above the control. |
hint | String | undefined | The quiet line under the control, shown while there is no error. |
required | Boolean | false | Marks the label with the required ornament. |
invalid | Boolean | false | Forces the invalid dress, outside any engine's verdict. |
disabled | Boolean | false | Mutes the slot. |