Build a Custom Module
Register a module in your application's trusted JavaScript bundle when a field needs additional browser behaviour. This example keeps a range input's output label in sync and removes its listener when the field disappears.
The field template must include data-formie-field-uid with the form-field instance UID, a range input and an output element. Formie's standard field wrapper supplies the UID attribute.
import { createFormieClient, type BrowserModuleDefinition } from '@verbb/formie-browser';
const rating: BrowserModuleDefinition = {
moduleId: 'acme:rating',
version: 2,
surfaces: ['server-rendered'],
kind: 'field',
match: ({ target }) => Boolean(target.querySelector('input[type="range"]')),
async setup({ target }) {
const input = target.querySelector<HTMLInputElement>('input[type="range"]')!;
const output = target.querySelector('output')!;
const sync = () => { output.textContent = input.value; };
input.addEventListener('input', sync);
sync();
return { destroy: () => input.removeEventListener('input', sync) };
},
};
const client = createFormieClient();
client.registerModule(rating);
await client.scan(document);Declare its configuration in your PHP field's defineBrowserModules() method:
use verbb\formie\models\BrowserModule;
protected function defineBrowserModules(): array
{
return [new BrowserModule([
'key' => $this->uid . ':rating-primary',
'moduleId' => 'acme:rating',
'surfaces' => [BrowserModule::SURFACE_SERVER_RENDERED],
'config' => ['max' => 10],
'required' => true,
])];
}Formie supplies the field target and includes the entry in the surface-specific manifest. Keep credentials and server-only settings out of config. The manifest cannot load a src URL; module code must already be registered or be resolved by Formie's trusted built-in import map.
For behaviour belonging to the whole form rather than a field, contribute a BrowserModule through the PHP manifest builder's registerModules event. The event exposes the form and requested surface. Contributions default to a form target and core kind, and pass through the same surface filtering and entry-key validation as built-in declarations. You do not need a dummy field to attach form-level behaviour.
For React, Vue and Web Components, register trusted definitions with clientRenderedModuleRegistry.register(definition) before mounting the component. The shared host owns reconciliation even when you replace the form component. Framework-independent consumers can pass their own ModuleRegistry to mountClientRenderedModules(root, instance, registry). Declare client-rendered in both the PHP entry and executable definition only when the module supports that component's markup. Keep <FormieForm> for server-rendered forms and <FormieClientForm> for the structured client-rendered product.