Browse all documentation

Custom field components

Replace a field input with your own Svelte component, starting with a title that counts characters.

A custom field component changes the control people use to edit a field. For example, a text field can show a character counter, a color preview, or a button that fills in a suggested value. The field still stores text, and its Go validation rules still apply when someone saves.

This tutorial adds a character counter below an article’s title. Start with an existing Ridu application and its posts collection.

Configuration

Option or prop Required What it does
defineFieldEditor({ type, component }) Yes Registers a Svelte editor for one supported built-in field type.
decodeConfig(value) When Go supplies settings Validates unknown JSON synchronously and returns typed component settings.
fields['app:name'] Yes Gives the local editor the same key selected in Go.
field.Admin.Editor Yes on each selected field Uses field.Component("app:name"), with optional JSON-safe settings.
FieldEditorProps.field.value / .set(value) Supplied by Ridu Reads and updates this field’s unsaved form value.
field.inputProps, field.readOnly, field.issues Supplied by Ridu Connects labels/errors, enforces read-only UI, and exposes validation feedback.
field.liveValidation / field.stale Supplied by Ridu Reports server-check state and whether an asynchronous result still belongs to this editor.

Supported types are text, textarea, email, date, code, number, checkbox, text list, and number list.

1. Create the component

Create admin/src/components/title-field.svelte:

admin/src/components/title-field.svelte
<script lang="ts">
  import type { FieldEditorProps } from '@riducms/plugin/editor';
  import Field from '@riducms/plugin/editor/field';
  import { Input } from '@riducms/ui';

  // Read and update the current form value through Ridu.
  let { field }: FieldEditorProps<'text'> = $props();
</script>

<!-- Field connects the input to its label and validation messages. -->
<Field {field}>
  <Input
    {...field.inputProps}
    value={field.value ?? ''}
    readonly={field.readOnly}
    oninput={(event) => field.set(event.currentTarget.value)}
  />
  <p>{(field.value ?? '').length} characters</p>
</Field>

Ridu passes the current field to your component as a prop. field.value contains what the user has typed, including changes they have not saved. field.set(...) updates that value in the form; the normal Save button saves the document.

Field adds the label, description, and validation messages. Spreading field.inputProps onto the input connects it to that label and those messages. readonly={field.readOnly} makes the input respect the field’s current editing permissions.

The character count reads the same value as the input, so it updates as the user types. There is no second copy of the title to keep in sync.

2. Register the component

In admin/src/admin.config.ts, import the component and add it to fields:

admin/src/admin.config.ts
import { defineAdmin } from '@riducms/plugin/admin';
import { generatedAdminPlugins } from './ridu.plugins.generated';
import { defineFieldEditor } from '@riducms/plugin/editor';
import TitleField from './components/title-field.svelte';

export default defineAdmin({
  plugins: generatedAdminPlugins,
  fields: {
    // Use this name in the Go field's Editor option.
    'app:titleCounter': defineFieldEditor({
      type: 'text',
      component: TitleField
    })
  }
});

app:titleCounter is the name you will use in Go. The app: prefix means this component belongs to your application. The rest of the name starts with a letter and can contain letters, numbers, and underscores.

type: 'text' tells Ridu which kind of field this component edits. It matches FieldEditorProps<"text"> in the component.

defineFieldEditor returns the registration stored in the fields map. defineAdmin collects that registration with the rest of your admin settings. Neither helper renders the component here; Ridu renders it when an editor opens a Go field that selects its name.

The helper reference lists every option, including how to pass per-field settings with decodeConfig. FieldEditorProps documents the value, validation messages, form reads, and other props your component receives.

3. Choose it for a field

Set Admin.Editor to field.Component("app:titleCounter"). For example:

content/posts.go
package content

import (
  "github.com/riducms/ridu"
  "github.com/riducms/ridu/field"
)

var Posts = ridu.Collection{
  Slug: "posts",
  Admin: ridu.CollectionAdmin{
    UseAsTitle:     "title",
    DefaultColumns: []string{"title", "readingMinutes"},
  },
  Fields: field.Fields{
    field.Text("title").
      Required().
      Admin(field.Admin{
        Editor: field.Component("app:titleCounter"),
      }),
    field.Number("readingMinutes"),
  },
}

If you are adding this collection for the first time, include content.Posts in your Go config’s Collections list, as shown in the Quickstart. If Posts already exists, keep its other fields and options and configure Admin.Editor on its title.

Add Editor to the field’s existing field.Admin settings if it already has any. Calling .Admin(...) a second time replaces all of those settings, including its description and width.

4. Try it

Keep the development server running. If it is not already running, start it from the project root:

terminal
bun run dev

This runs ridu dev, which regenerates the schema when your Go fields change and reloads the admin when you edit components or their registration. You do not need to run ridu generate separately or restart the development server for these changes.

Open Posts and create or edit a document. Type a title: the character count should update immediately. Save, then reload the page; the title and count should still match. Clearing a required title and saving should display the usual validation message.

Run bun run check when you are ready to validate your work before committing or building. It is a separate check, not a prerequisite for trying the component.

The same component can be selected on text fields in groups, arrays, and blocks. It follows its row when the row is reordered.

Show server feedback while editing

Add .LiveValidate(...) to the Go field to enable server checks while editing. Updating through field.set(...) starts the check; the Field wrapper displays returned issues, and the surrounding admin handles checking and retry messages.

Your component can read field.liveValidation.status when it needs to react to a pending or failed check. Ridu handles the requests and cancellation. Keep .Validate(...) registered when the rule must also prevent an invalid save.

Other field types

Application field components currently support text, textarea, email, date, code, number, and checkbox fields. Match the Go field, the registration’s type, and FieldEditorProps. A number component reads and writes numbers; a checkbox component reads and writes booleans. The other supported types use strings. Use field.set(null) to clear a value.

For controls without an HTML read-only mode, such as checkboxes, use disabled={field.readOnly}. Ridu also checks whether editing is allowed when field.set is called.

To change an array row’s heading, use Row labels. Selects, relationships, uploads, JSON, and rich text cannot use a component registered this way. Their custom editors need a plugin.

Read another field or wait for an API response

The component can also receive form. For example, form.get('subtitle') reads the current subtitle, including unsaved edits. This interface reads other fields; use field.set to change the field your component edits.

For a suggestion button that waits for an API response, keep the field object you started with and check field.stale before applying the result. It becomes stale if the user closes the editor, removes the row, changes document or locale, or saves or resets the form. Discard that result instead of writing it into the new form.

For reusable settings supplied from Go, see field.Component and defineFieldEditor. Settings are checked with a decodeConfig function before they reach your component.