Headless Core
Recipes
This cookbook covers common form workflows.
📚 Basic Form with Schema Validation
Goal: Validate a simple login form and submit safely.
type LoginForm = {
email: string;
password: string;
};
const formController = new FormController<LoginForm>({
initialData: {
email: "",
password: "",
},
validationSchema: loginSchema, // StandardSchemaV1
});
const emailField = formController.registerField(form.path.of("email"));
const passwordField = formController.registerField(form.path.of("password"));
const handleSubmit = formController.createSubmitHandler(async (data) => {
await api.login(data);
});
formEl.onsubmit = handleSubmit;
📚 Default Values for Dynamically Mounted Fields
Goal: Provide fallback values when a field appears conditionally.
const field = formController.registerField(
formController.path.of("profile.bio"),
{ defaultValue: "Hello there 👋" },
);
If data.profile.bio is null | undefined, it gets initialized.
To also modify initialData:
formController.registerField(formController.path.of("profile.bio"), {
defaultValue: "Hello there 👋",
overrideInitialValue: true,
});
📚 On-Blur Field Validation
Goal: Validate only the field the user just left.
const path = formController.path.of("email");
input.onblur = async () => {
await formController.validateField(path);
};
📚 Reset With New Server Data
Goal: Rehydrate form after fetching user data.
const user = await api.getUser();
formController.reset(user);
This:
- Replaces
initialData - Recreates data
- Clears issues
- Resets dirty/touched state
- Keeps fields registered
Perfect for edit forms.
📚 Conditional Fields
Goal: Dynamically add/remove fields.
const addressPath = formController.path.of("address", "street");
if (showAddress) {
formController.registerField(addressPath);
} else {
formController.unregisterField(addressPath);
}
Data remains intact — only the field abstraction changes.
📚 Disable Submit Button Reactively
formController.events.on("submissionStatusChange", (isSubmitting) => {
submitButton.disabled = isSubmitting;
});
Or reflect validation:
formController.events.on("validationStatusChange", (isValidating) => {
spinner.visible = isValidating;
});
📚 Force Dirty on Change
formController.events.on("fieldValueChanged", (path, newValue, oldValue) => {
const field = formController.getField(path);
if (field != null) {
// Normally dirtiness is determined by comparing current value to initial value.
// This forces it to be dirty regardless of that comparison.
field.markDirty();
}
});