Skills for AEM Forms on Edge Delivery Services: building forms with the Form block.
96%
Does it follow best practices?
Impact
—
Average score across 6 eval scenarios
Passed
No findings from the security scan
This guide explains how to create custom components for the Form block, which follows an MVC (Model-View-Controller) architecture. It covers the structure, authorable properties, extension mechanism, and best practices for custom components.
blocks/form/models/form-components/_<type>.json for OOTB fields, blocks/form/components/<name>/_<name>.json for custom components).blocks/form/models/form-components._text-input.json, _drop-down.json).fd).blocks/form/components folder.countdown-timer).countdown-timer.js (main logic)countdown-timer.css (optional, for styles)_<name>.json in the component folder (blocks/form/components/<name>/) that extends an existing component definition.properties or fields section._countdown-timer.json for a real-world example. This file defines a custom component that extends the base button component and adds new properties like initialText, finalText, time, and retries.fields array of the JSON file, making them available to authors in the form builder.:type property, which can be set as fd:viewType in the JSON file (e.g., fd:viewType: countdown-timer). This allows the system to recognize and load the correct custom component.properties.<propertyName> in your component's JS logic.:type (set via fd:viewType in the JSON schema) — checked against both customComponents and OOTBComponentDecorators in mappings.js.properties.variant (set in authoring) — checked against customComponents only.blocks/form/components/<name>/.decorate).export default function decorate(element, fd, container, formId) {
// element: The HTML structure of the OOTB component you are extending
// fd: The JSON field definition (all authorable properties)
// container: The parent element (fieldset or form)
// formId: The id of the form
// ... your logic here ...
}element, add event listeners, inject additional markup, etc.fd.properties.<propertyName>.subscribe Workssubscribe function (from blocks/form/rules/index.js) connects your component to its field model. Always use { listenChanges: true } for new components.callback(element, fieldModel, 'register') — called once when the model is readycallback(element, fieldModel, 'change', payload) — called on every property change (when listenChanges is true)payload.changes is an array of { propertyName, currentValue, prevValue }import { subscribe } from '../../rules/index.js';
export default function decorate(fieldDiv, fieldJson, container, formId) {
const { initialText, finalText, time } = fieldJson?.properties;
let model = null;
subscribe(fieldDiv, formId, (_fieldDiv, fieldModel, eventType, payload) => {
if (eventType === 'register') {
model = fieldModel;
// one-time setup...
} else if (eventType === 'change') {
payload?.changes?.forEach((change) => {
if (change?.propertyName === 'value') {
// react to value changes
}
});
}
}, { listenChanges: true });
}subscribe() on each child's DOM wrapper inside the parent's 'register' callback. Find child wrappers with element.querySelector('[data-id="${child.id}"]') and find child models by fieldType or ':type'. See subscribe-api.md for the full child pattern.idVerified, timerComplete) dispatched from rules, custom functions, or other components: use fieldModel.subscribe(cb, 'eventName') inside the 'register' branch — not fieldDiv.addEventListener(...). DOM listeners bypass the AEM Forms model event system. See subscribe-api.md § Custom event subscription for the full pattern, dispatch sites, and anti-pattern callout.When defining fields in your custom component's JSON (for any field group—basic, validation, help, etc.), follow these best practices for maintainability and consistency:
../../models/form-common/_basic-input-placeholder-fields.json#/fields, ../../models/form-common/_basic-validation-fields.json#/fields). This ensures you inherit all standard options without duplicating them.../../models/form-common/_help-container.json) as needed for consistency and maintainability.Minimal Example: Container in a Custom Component JSON
{
"component": "container",
"name": "validation",
"label": "Validation",
"collapsible": true,
"fields": [
{ "...": "../../models/form-common/_basic-validation-fields.json#/fields" },
{ "component": "number", "name": "minAge", "label": "Minimum Age", "valueType": "number", "description": "Minimum age allowed for date of birth." },
{ "component": "number", "name": "maxAge", "label": "Maximum Age", "valueType": "number", "description": "Maximum age allowed for date of birth." },
{ "component": "text", "name": "minimumErrorMessage", "label": "Minimum age error message", "valueType": "string", "description": "Error message shown when the age is below the minimum allowed." },
{ "component": "text", "name": "maximumErrorMessage", "label": "Maximum age error message", "valueType": "string", "description": "Error message shown when the age is above the maximum allowed." }
]
}Tip:
blocks/form/components with your component's name (e.g., countdown-timer).blocks/form/components/countdown-timer/countdown-timer.jsblocks/form/components/countdown-timer/countdown-timer.css_countdown-timer.json) in the same folder as your component JS file (blocks/form/components/countdown-timer/_countdown-timer.json). This JSON should extend an existing component and set fd:viewType to your component's name (see minimal example above).
element parameter to modify the base HTML structure.fd parameter if needed for standard field data.subscribe to listen to field changes or custom events if needed.fd:viewType in your component's JSON schema to your component's name (e.g., countdown-timer). The runtime uses this as the :type property to load the component. Alternatively, components can be identified via properties.variant set in authoring (checked against customComponents only).mappings.js: Add your component's name to the OOTBComponentDecorators (for OOTB-style components) or customComponents list so it is recognized and loaded by the system._form.json: Add your component's name to the "form" filter components array so authors can insert it in the Universal Editor (the project scaffolder does this automatically).npm run build:json to compile and merge all component JSON definitions into a single file to be served from the server. This ensures your new component's schema is included in the merged output.JS implementation (countdown-timer.js):
import { subscribe } from '../../rules/index.js';
export default function decorate(fieldDiv, fieldJson, container, formId) {
const { initialText, finalText, time } = fieldJson?.properties;
let model = null;
subscribe(fieldDiv, formId, (_fieldDiv, fieldModel, eventType, payload) => {
if (eventType === 'register') {
model = fieldModel;
// one-time setup with model...
} else if (eventType === 'change') {
payload?.changes?.forEach((change) => {
if (change?.propertyName === 'value') {
// react to value changes
} else if (change?.propertyName === 'properties') {
// react to custom property changes (e.g., initialText)
}
});
}
}, { listenChanges: true });
}blocks/form/models/form-components: OOTB component property definitions.blocks/form/components: Place for your custom components.blocks/form/components/countdown-timer/_countdown-timer.json shows how to extend a base component and add new properties.By following this guide, you can create robust, maintainable custom components that integrate seamlessly with the Form block's architecture.