Docs Components Uncategorized

Form

React example follows below.
ts
import { useForm } from "@grassroot/form-react";
import { Button, Form, Input } from "@grassroot/ui-react";
import * as React from "react";
const validateEmail = (value: unknown) =>
String(value).includes("@") ? [] : [{ path: "email", message: "Enter a valid email address." }];
export default function FormDemo() {
const [savedEmail, setSavedEmail] = React.useState("");
const form = useForm({
defaultValues: { email: "" },
validators: { email: validateEmail },
validationPolicy: {
shouldValidate: (event, hasError = false) =>
event === "blur" || (event === "change" && hasError),
},
onSubmit: (values) => setSavedEmail(String(values.email ?? "")),
});
const email = form.useField("email");
const formBinding = {
formProps: () => ({
noValidate: true,
onSubmit: (event: React.SubmitEvent<HTMLFormElement>) =>
form.formProps().onSubmit(event.nativeEvent),
}),
};
return (
<div className="w-full max-w-md" data-form-demo>
<Form form={formBinding} data-status={form.service.getSnapshot().context.status}>
<Input
name={email.inputProps.name}
value={email.inputProps.value}
onInput={(event) => email.inputProps.onInput(event.nativeEvent)}
onBlur={email.inputProps.onBlur}
label="Work email"
hint="Deployment alerts will be sent here."
error={email.error?.[0]?.message}
type="email"
autoComplete="email"
required
/>
<div className="flex flex-wrap items-center gap-3">
<Button type="submit">Save email</Button>
<p className="m-0 min-h-5 text-[12.5px] text-muted" role="status" aria-live="polite">
{savedEmail
? `Saved ${savedEmail} for this preview.`
: "No email saved in this preview."}
</p>
</div>
</Form>
</div>
);
}
ts
<script setup lang="ts">
import { computed, ref } from "vue";
import { useForm } from "@grassroot/form-vue";
import { Button, Form, Input } from "@grassroot/ui-vue";
const validateEmail = (value: unknown) =>
String(value).includes("@") ? [] : [{ path: "email", message: "Enter a valid email address." }];
const savedEmail = ref("");
const form = useForm(() => ({
defaultValues: { email: "" },
validators: { email: validateEmail },
validationPolicy: {
shouldValidate: (event, hasError = false) =>
event === "blur" || (event === "change" && hasError),
},
onSubmit: (values) => {
savedEmail.value = String(values.email ?? "");
},
}));
const email = form.field("email");
const emailValue = computed(() => String(form.snapshot.value.context.values.email ?? ""));
const emailError = computed(() => {
void form.snapshot.value;
return email.error()?.[0]?.message;
});
</script>
<template>
<div class="w-full max-w-md" data-form-demo>
<Form :form="form" :data-status="form.snapshot.value.context.status">
<Input
:model-value="emailValue"
:name="email.inputProps().name"
label="Work email"
hint="Deployment alerts will be sent here."
:error="emailError"
type="email"
autocomplete="email"
required
@input="email.inputProps().onInput($event)"
@blur="email.inputProps().onBlur()"
/>
<div class="flex flex-wrap items-center gap-3">
<Button type="submit">Save email</Button>
<p class="m-0 min-h-5 text-[12.5px] text-muted" role="status" aria-live="polite">
{{
savedEmail ? `Saved ${savedEmail} for this preview.` : "No email saved in this preview."
}}
</p>
</div>
</Form>
</div>
</template>
ts
import "./ensure-compiler";
import { Component, computed, signal } from "@angular/core";
import { injectForm } from "@grassroot/form-angular";
import { GrassrootButton, GrassrootForm, GrassrootInput } from "@grassroot/ui-angular";
const validateEmail = (value: unknown) =>
String(value).includes("@") ? [] : [{ path: "email", message: "Enter a valid email address." }];
@Component({
selector: "app-form-demo",
standalone: true,
imports: [GrassrootButton, GrassrootForm, GrassrootInput],
host: { style: "display: contents" },
template: `
<div class="w-full max-w-md" data-form-demo>
<grassroot-form [form]="form" [attr.data-status]="form.snapshot().context.status">
<grassroot-input
[name]="email.inputProps().name"
[value]="emailValue()"
(valueChange)="inputEmail($event)"
(focusout)="blurEmail()"
label="Work email"
hint="Deployment alerts will be sent here."
[error]="emailError()"
type="email"
autocomplete="email"
[required]="true"
/>
<div class="flex flex-wrap items-center gap-3">
<grassroot-button type="submit">Save email</grassroot-button>
<p class="m-0 min-h-5 text-[12.5px] text-muted" role="status" aria-live="polite">
{{
savedEmail()
? "Saved " + savedEmail() + " for this preview."
: "No email saved in this preview."
}}
</p>
</div>
</grassroot-form>
</div>
`,
})
export class FormDemoComponent {
readonly savedEmail = signal("");
readonly form = injectForm({
defaultValues: { email: "" },
validators: { email: validateEmail },
validationPolicy: {
shouldValidate: (event, hasError = false) =>
event === "blur" || (event === "change" && hasError),
},
onSubmit: (values) => this.savedEmail.set(String(values.email ?? "")),
});
readonly email = this.form.field("email");
readonly emailValue = computed(() => String(this.form.snapshot().context.values.email ?? ""));
readonly emailError = computed(() => {
this.form.snapshot();
return this.email.error()?.[0]?.message;
});
inputEmail(value: string): void {
this.email.inputProps().onInput({ target: { value } } as unknown as Event);
}
blurEmail(): void {
this.email.inputProps().onBlur();
}
}
export default FormDemoComponent;
ts
<script lang="ts">
import { onDestroy } from "svelte";
import { createForm } from "@grassroot/form-svelte";
import { Button, Form, Input } from "@grassroot/ui-svelte";
const validateEmail = (value: unknown) =>
String(value).includes("@") ? [] : [{ path: "email", message: "Enter a valid email address." }];
let savedEmail = $state("");
const form = createForm({
defaultValues: { email: "" },
validators: { email: validateEmail },
validationPolicy: {
shouldValidate: (event, hasError = false) =>
event === "blur" || (event === "change" && hasError),
},
onSubmit: (values) => {
savedEmail = String(values.email ?? "");
},
});
const email = form.field("email");
let snapshot = $state(form.service.getSnapshot());
const unsubscribe = form.service.subscribe(
(value) => value,
(value) => (snapshot = value),
);
let emailValue = $derived(String(snapshot.context.values.email ?? ""));
let emailError = $derived.by(() => {
snapshot;
return email.error()?.[0]?.message;
});
function inputEmail(event: Event) {
email.inputProps().onInput(event);
}
onDestroy(() => {
unsubscribe();
form.destroy();
});
</script>
<div class="w-full max-w-md" data-form-demo>
<Form {form} data-status={snapshot.context.status}>
<Input
value={emailValue}
name={email.inputProps().name}
label="Work email"
hint="Deployment alerts will be sent here."
error={emailError}
type="email"
autocomplete="email"
required
oninput={inputEmail}
onblur={email.inputProps().onBlur}
/>
<div class="flex flex-wrap items-center gap-3">
<Button type="submit">Save email</Button>
<p class="m-0 min-h-5 text-[12.5px] text-muted" role="status" aria-live="polite">
{savedEmail ? `Saved ${savedEmail} for this preview.` : "No email saved in this preview."}
</p>
</div>
</Form>
</div>
ts
/** @jsxImportSource solid-js */
import { createForm } from "@grassroot/form-solid";
import { Button, Form, Input } from "@grassroot/ui-solid";
import { createSignal, type JSX } from "solid-js";
const validateEmail = (value: unknown) =>
String(value).includes("@") ? [] : [{ path: "email", message: "Enter a valid email address." }];
export default function FormDemo(): JSX.Element {
const [savedEmail, setSavedEmail] = createSignal("");
const form = createForm({
defaultValues: { email: "" },
validators: { email: validateEmail },
validationPolicy: {
shouldValidate: (event, hasError = false) =>
event === "blur" || (event === "change" && hasError),
},
onSubmit: (values) => {
setSavedEmail(String(values.email ?? ""));
},
});
const email = form.field("email");
const emailError = () => {
form.snapshot();
return email.error()?.[0]?.message;
};
return (
<div class="w-full max-w-md" data-form-demo>
<Form form={form} data-status={form.snapshot().context.status}>
<Input
name={email.inputProps().name}
value={String(form.snapshot().context.values.email ?? "")}
onInput={email.inputProps().onInput}
onBlur={email.inputProps().onBlur}
label="Work email"
hint="Deployment alerts will be sent here."
error={emailError()}
type="email"
autocomplete="email"
required
/>
<div class="flex flex-wrap items-center gap-3">
<Button type="submit">Save email</Button>
<p class="m-0 min-h-5 text-[12.5px] text-muted" role="status" aria-live="polite">
{savedEmail()
? `Saved ${savedEmail()} for this preview.`
: "No email saved in this preview."}
</p>
</div>
</Form>
</div>
);
}

Installation

API Reference

Import

import { useForm } from "@grassroot/form-react";
import { Button, Form, Input } from "@grassroot/ui-react";
import * as React from "react";

Props

Prop
Type
Default
Requirement
Description
form
{ readonly formProps: () => React.FormHTMLAttributes<HTMLFormElement>; } | undefined
optional
Form API props returned by
Prop
form
Type
{ readonly formProps: () => React.FormHTMLAttributes<HTMLFormElement>; } | undefined
Default
Requirement
optional
Description
Form API props returned by

Content regions

  • children

Native attributes

Standard DOM and ARIA attributes not listed above spread onto the root element.

Import

import { computed, ref } from "vue";
import { useForm } from "@grassroot/form-vue";
import { Button, Form, Input } from "@grassroot/ui-vue";

Props

Prop
Type
Default
Requirement
form
{ formProps: () => Record<string, unknown>; } | undefined
optional
Prop
form
Type
{ formProps: () => Record<string, unknown>; } | undefined
Default
Requirement
optional

Content regions

  • default

Import

import "./ensure-compiler";
import { Component, computed, signal } from "@angular/core";
import { injectForm } from "@grassroot/form-angular";
import { GrassrootButton, GrassrootForm, GrassrootInput } from "@grassroot/ui-angular";

Props

Prop
Type
Default
Requirement
form
{ readonly formProps: () => { readonly noValidate?: boolean; readonly onSubmit?: (event: SubmitEvent) => void; }; }
optional
Prop
form
Type
{ readonly formProps: () => { readonly noValidate?: boolean; readonly onSubmit?: (event: SubmitEvent) => void; }; }
Default
Requirement
optional

Content regions

  • default

Import

import { onDestroy } from "svelte";
import { createForm } from "@grassroot/form-svelte";
import { Button, Form, Input } from "@grassroot/ui-svelte";

Props

Prop
Type
Default
Requirement
form
{ formProps: () => Record<string, unknown> }
optional
class
string
optional
Prop
form
Type
{ formProps: () => Record<string, unknown> }
Default
Requirement
optional
Prop
class
Type
string
Default
Requirement
optional

Content regions

  • children

Import

import { createForm } from "@grassroot/form-solid";
import { Button, Form, Input } from "@grassroot/ui-solid";
import { createSignal, type JSX } from "solid-js";

Props

Prop
Type
Default
Requirement
form
{ readonly formProps: () => JSX.FormHTMLAttributes<HTMLFormElement> }
optional
Prop
form
Type
{ readonly formProps: () => JSX.FormHTMLAttributes<HTMLFormElement> }
Default
Requirement
optional

Content regions

  • children

Native attributes

Standard DOM and ARIA attributes not listed above spread onto the root element.