Skip to main content

Plugins

Plugins extend the form engine with new field types, operators, effects, validators, and data sources. All built-in behavior is implemented as plugins.

Built-in plugins

import { registerBasicPlugins, registerAsyncPlugins } from "formwright/plugins";

const runtime = createFormRuntime({
form,
plugins: [
...registerBasicPlugins(), // operators (eq, gt, in, ...) + effects (show, hide, require, ...)
...registerAsyncPlugins(), // static + remote datasource loaders
],
});

You can also pass registerAsyncPlugins({ baseUrl: "https://api.example.com" }) to set a base URL for relative endpoints.

Plugin kinds

KindPurpose
fieldCustom field types with normalization, default values, validation, serialization
layoutCustom layout node types
operatorNew condition operators (fuzzy, startsWith, isToday, ...)
effectNew rule effects (notify, setProp, ...)
validatorCustom validation rules
datasourceCustom option loaders

Writing an operator plugin

Add a new operator for conditions in rules. Example: startsWith:

import type { OperatorPlugin } from "formwright/core";

const startsWithOperator: OperatorPlugin = {
kind: "operator",
identity: { name: "startsWith", version: "1" },
operatorType: "startsWith",

evaluate({ expression, values, context }) {
// expression is { startsWith: [{ var: "fieldPath" }, "prefix"] }
const [ref, prefix] = expression.startsWith as [{ var: string }, string];
const value = values[ref.var];
return typeof value === "string" && value.startsWith(prefix);
},
};

const runtime = createFormRuntime({
form,
plugins: [
...registerBasicPlugins(),
startsWithOperator,
],
});

Use it in schema rules:

import type { RuleExpression } from "formwright/schema";

rules: [
rule.when({ startsWith: [{ var: "email" }, "admin@"] } as RuleExpression).show(adminPanel),
]

Writing an effect plugin

Add a new effect. Example: setReadonly:

import type { EffectPlugin } from "formwright/core";

const setReadonlyEffect: EffectPlugin = {
kind: "effect",
identity: { name: "setReadonly", version: "1" },
effectType: "setReadonly",

apply({ effect }) {
// effect is { type: "setReadonly", target: "fieldPath", value: true }
return {
fieldMutations: [
{
path: effect.target as string,
patch: { readonly: Boolean(effect.value) },
},
],
};
},
};

Writing a field plugin

Field plugins add new field types with custom normalization, validation, and serialization.

import type { FieldPlugin } from "formwright/core";

const phoneFieldPlugin: FieldPlugin = {
kind: "field",
identity: { name: "phone-field", version: "1" },
fieldType: "phone",

normalize(input) {
return {
fieldType: "phone",
normalizedDataField: input.dataField,
normalizedUiField: {
...input.uiField,
fieldType: "phone",
},
};
},

getDefaultValue() {
return "";
},

getValidationPlan(input) {
return input.dataField.required
? [{ validatorType: "required" }]
: [];
},

getRendererKey() {
return "phone";
},

serialize(input) {
return input.value;
},

deserialize(input) {
return input.value;
},
};

Then use fieldType: "phone" in your custom field definition.

Plugin identity

Every plugin requires an identity with a name and version. Two plugins with the same name will throw a DuplicatePluginError. Use a namespaced name to avoid conflicts:

identity: { name: "my-app/phone-field", version: "1" }