Architecture
Overview
Kin Form is built on a hierarchical and compositional architecture, making it flexible enough to handle everything from simple inputs to complex, deeply nested forms.
The architecture consists of three main components:
-
FormField
: The base class for all individual form fields (e.g., a text input, a checkbox, a select dropdown). It manages the value, validation, and status (touched, invalid, etc.) of a single piece of data. -
FieldGroup
: A special type of field that contains other fields. It allows you to group fields together into a nested object structure. It aggregates the status of its children; for example, aFieldGroup
is considered invalid if any of its child fields are invalid. -
FormController
: The main controller that manages the entire form. It is aFieldGroup
with additional logic for handling form submission, reset, and top-level state likedirty
andsubmitting
.
This structure allows you to compose complex forms by nesting FormField
and FieldGroup
components.
How It Works in Practice
You typically instantiate a FormController
in your Lit component. The controller then uses the field()
directive to link parts of its data model to FormField
or FieldGroup
elements in your template.
interface Model {
email: string;
password: string;
}
class LoginPage extends LitElement {
// The controller manages the form's state.
#form = new FormController<Model>(this, {
initialValue: { email: "", password: "" },
onSubmit: (form) => {
console.log("Form submitted!", form.value);
},
});
protected override render(): unknown {
const { field, handleSubmit } = this.#form;
return html`
<!-- The field() directive links these fields to the controller -->
<text-field ${field("email")}></text-field>
<password-field ${field("password")}></password-field>
<button @click=${handleSubmit}>Log in</button>
`;
}
}
Design Rationale
In Kin Form, FormController
is implemented as a Lit ReactiveController
, but it is also technically a custom element. This design was chosen to reduce boilerplate and improve developer ergonomics.
An alternative approach would be to use it as a traditional custom element, like <kin-form>
.
// Alternative, more verbose approach
class LoginPage extends LitElement {
protected override render(): unknown {
return html`
<kin-form
.initialValue=${...}
.onSubmit=${...}
.template=${({field, handleSubmit}) => html`
<text-field ${field('email')}></text-field>
<button @click=${handleSubmit}>Log in</button>
`}
></kin-form>
`;
}
}
This alternative is slightly more verbose and can be less efficient, as the template function may be recreated on each render. The current approach, using a ReactiveController
, provides a more direct and streamlined way to manage form state within a Lit component.