Form & Field Hooks
Use Dyrected's form and field state APIs to build custom admin inputs without reinventing nested path handling, validation wiring, or read-only behavior.
If you are building UI that edits a Dyrected document, you usually do not want to rebuild the hard parts yourself.
That includes things like:
- keeping a whole document form in sync
- building a custom field input
- editing nested object or array values
- showing validation and dirty state
- respecting read-only mode automatically
That is what the form and field APIs are for.
By the end of this page you should know when to use the form API, when to use the field API, and how the React and Vue setup differs.
Start with the level of state you need
There are two levels of state:
- form state for the whole document
- field state for one path inside that document
Use the form API when your component needs to understand the document as a whole.
Use the field API when your component only cares about one field or one nested subtree.
The field API is built for the parts that are annoying to get right by hand:
- nested dotted paths
- object children
- array item paths
- block-style nested values
- dirty and validation state at the field level
In normal component code, the hook or composable is the API you work with.
What these APIs save you from
Without a shared form API, custom field code usually ends up re-solving the same problems over and over:
- where the current value lives
- how nested paths are resolved
- how validation state is surfaced
- how read-only mode is respected
- how one field updates without breaking the rest of the document
Dyrected's public form and field APIs already solve those problems, so your custom components can focus on the UI instead.
Typical setups
In React, the field APIs are available from @dyrected/react, but they are mounted through a provider.
That means the usual flow is:
- create a form controller once
- pass it to
DyrectedFormProvider - call
useDyrectedForm()oruseField()inside descendants
import {
DyrectedFieldPathProvider,
DyrectedFormProvider,
useDyrectedForm,
useField,
} from "@dyrected/react";
import { createDyrectedFormController } from "@dyrected/admin/public";
const controller = createDyrectedFormController({
collection: "posts",
fields: [
{ name: "title", label: "Title", type: "text" },
{
name: "hero",
label: "Hero",
type: "object",
fields: [{ name: "headline", label: "Headline", type: "text" }],
},
],
initialValues: {
title: "Hello",
hero: { headline: "Welcome" },
},
});
function TitleField() {
const field = useField("title");
return (
<input
value={String(field.value ?? "")}
onChange={(event) => field.setValue(event.target.value)}
/>
);
}
function HeadlineField() {
const field = useField();
return (
<input
value={String(field.value ?? "")}
onChange={(event) => field.setValue(event.target.value)}
/>
);
}
export function PostForm() {
const form = useDyrectedForm();
return (
<form
onSubmit={(event) => {
event.preventDefault();
void form.submit();
}}
>
<TitleField />
<DyrectedFieldPathProvider path="hero.headline">
<HeadlineField />
</DyrectedFieldPathProvider>
</form>
);
}
export function PostFormRoot() {
return (
<DyrectedFormProvider controller={controller}>
<PostForm />
</DyrectedFormProvider>
);
}In Vue, the same split exists, but the setup boundary uses provideDyrectedForm() and provideDyrectedFieldPath() instead of React providers.
<script setup lang="ts">
import {
provideDyrectedFieldPath,
provideDyrectedForm,
useDyrectedForm,
useField,
} from "@dyrected/vue";
import { createDyrectedFormController } from "@dyrected/admin/public";
const controller = createDyrectedFormController({
collection: "posts",
fields: [
{ name: "title", type: "text" },
{
name: "hero",
type: "object",
fields: [{ name: "headline", type: "text" }],
},
],
initialValues: {
title: "Hello",
hero: { headline: "Welcome" },
},
});
provideDyrectedForm(controller);
const form = useDyrectedForm();
const title = useField("title");
provideDyrectedFieldPath("hero.headline");
const headline = useField();
</script>
<template>
<form @submit.prevent="form.submit()">
<input
:value="String(title.value.value ?? '')"
@input="title.setValue(($event.target as HTMLInputElement).value)"
/>
<input
:value="String(headline.value.value ?? '')"
@input="headline.setValue(($event.target as HTMLInputElement).value)"
/>
</form>
</template>In Vue, the semantic contract matches React, but the state fields are refs.
useDyrectedForm
Use useDyrectedForm when the component cares about the whole document:
- submit state
- dirty state
- validation state
- full values
- looking up one field schema or one field state from the document root
This is the right API for:
- custom document shells
- custom side panels
- save bars
- validation summaries
- any component that needs more than one field at a time
useField
Use useField when the component is responsible for one field path.
This is the right API for:
- custom field inputs
- object field sections
- array row editors
- block-level UI
useField also includes child-path helpers such as:
getChildPathgetItemPathgetChildValuesetChildValue
Those helpers are what save you from hand-building nested dotted paths everywhere.
Built-in Declarative Jexl Helpers
Dyrected provides a zero-dependency helper utility suite for declarative admin.hooks.onChange, field conditions, and access control rules:
String Helpers
slugify(str): Transforms text into a clean URL-safe slug (slugify(siblingData.title)$\to$"hello-world").lower(str)/upper(str): Converts string casing.trim(str)/capitalize(str): Removes surrounding whitespace or capitalizes the first word.truncate(str, length, ellipsis?): Truncates text cleanly for previews or SEO meta descriptions.wordCount(str)/readingTime(str): Calculates word count and estimated reading time.replace(str, search, replacement)/startsWith(str, prefix)/endsWith(str, suffix): Pattern matching and replacement.
Date & Time Helpers
now(): Returns current ISO timestamp ("2026-07-27T00:00:00.000Z").today(): Returns current date ("2026-07-27").formatDate(date, style?): Formats timestamps into'short','iso','date','datetime', or'full'.addDays(date, days): Adds or subtracts days from a date.diffDays(dateA, dateB): Returns the integer difference in days between two dates.isPast(date)/isFuture(date): Returnstrueif a date is in the past or future.
Array & Collection Helpers
includes(arrOrStr, item): Checks array membership or substring inclusion.join(arr, separator?): Joins array elements with a delimiter.first(arr)/last(arr): Pick first or last element of an array safely.compact(arr)/unique(arr): Removes empty items or deduplicates arrays.length(val): Returns array length, string length, or object key count.
Logical & Object Helpers
default(val, fallback): Provides fallback for empty,null, orundefinedvalues.coalesce(...args): Returns first non-empty value.isEmpty(val): Returnstruefor empty arrays, objects, strings, ornull.get(obj, path, fallback?): Safely retrieves nested properties (get(siblingData, "author.name", "Guest")).
Recommended path
Reach for useDyrectedForm when your component needs document-level state, and useField when your component only cares about one field or one nested subtree.
Import them from @dyrected/react or @dyrected/vue, wire the one-time form boundary near the top of your editing UI, and then keep the rest of your code at the hook or composable level.
Hooks & Composables
Use Dyrected's hooks and composables to build media flows, editing UI, and themed product surfaces without rebuilding the state layer yourself.
Media Hooks & Composables
Build custom uploaders, URL import flows, and media pickers with `@dyrected/react` and `@dyrected/vue` without reimplementing the media pipeline yourself.