# Formly Form Builder (Angular 21 + ngx-formly v7)
Production-oriented visual builder with a strict domain model and renderer-aware Formly export.
.github/workflows/pages.yml on push to master.Common Fields, Advanced Fields, and Layout.Panel, Row, Column, Tabs, Stepper, and Accordion.File, Slider, Date range, and Rating.My Templates category).loadComponent for the builder page.row accepts only col children.col can contain fields and nested rows.FormlyFieldConfig[] using Formly v7 props.npm install
npm startApp runs at http://localhost:4201.
Build libraries once, then run either renderer consumer app:
Example :npm run build:libs
npm run start:example:bootstrap
npm run start:example:materialhttp://localhost:4204http://localhost:4203Builder -> Viewer Flow example:BuilderDocumentformly-viewexamples/bootstrap-consumerexamples/material-consumernpm install @ngx-formly-builder/core @ngx-formly/core @ngx-formly/material @angular/materialOr use schematic setup:
Example :ng add @ngx-formly-builder/coreng add is implemented, but it is intentionally lightweight:
NGX_FORMLY_BUILDER_SETUP.md into the host project<formly-builder (configChange)="builderDoc = $event" />import { builderToFormly, type BuilderDocument } from '@ngx-formly-builder/core';
import type { FormlyFieldConfig } from '@ngx-formly/core';
builderDoc: BuilderDocument | null = null;
formlyFields: FormlyFieldConfig[] = [];
onConfigChange(doc: BuilderDocument): void {
this.builderDoc = doc;
this.formlyFields = builderToFormly(doc);
}See full guide: docs/features/getting-started-5-min.md.
Use read-only mode for review, approval, or embedded admin flows:
Example :<formly-builder [config]="builderDoc" [readOnly]="true" />In read-only mode, the builder keeps the canvas/preview visible but hides the editing chrome and blocks mutations.
If you only need runtime rendering without the builder shell, use formly-view:
<formly-view [config]="builderDoc" [model]="submittedData" [readOnly]="true" />npm start
npm run build
npm run build:lib
npm run pack:lib
npm run build:pages
npm test -- --watch=false --browsers=ChromeHeadless
npm run e2e
npm run e2e:smoke
npm run e2e:critical
npm run typecheck
npm run lint
npm run format:check
npm run docs
npm run storybook
npm run storybook:buildpre-commit: runs lint-staged (Prettier + ESLint fix on staged files).commit-msg: validates Conventional Commits via Commitlint..github/workflows/ci.yml) enforces: typecheck, lint, format check, test, build, docs.Export Formly JSON: production-consumable FormlyFieldConfig[].Export Builder JSON: internal builder document (round-trip format).Import Builder JSON: restores builder document.Import Formly JSON: maps Formly config into builder model.Import from schema...: uses registered schema adapters such as JSON Schema and OpenAPI.Export as schema...: uses registered schema adapters that support export.Export Templates JSON: exports saved field templates.Import Templates JSON: imports saved field templates (field node templates only).Input, Textarea, Email, Password, Phone, URLCheckbox, Radio, Select, Multi-selectNumber, Slider, Date, Date range, RatingFile, RepeaterPanel, Row, Column, Tabs, Stepper, AccordionCommon FieldsAdvanced FieldsLayoutBUILDER_PALETTE.BUILDER_PLUGINS:paletteItems extensions/overridesschemaAdapters extensions/overrideslookupRegistry extensions/overridesvalidatorPresets extensions (for custom field defaults)validatorPresetDefinitions extensions/overrides (for inspector-selectable named presets)formlyExtensions for custom Formly types/wrappers used by Preview dialogssrc/app/app.config.ts):import { BUILDER_PLUGINS, type BuilderPlugin } from './builder-core/plugins';
const CRM_PLUGIN: BuilderPlugin = {
id: 'crm',
paletteItems: [
{
id: 'crm-customer-id',
category: 'Common Fields',
title: 'Customer ID',
nodeType: 'field',
fieldKind: 'input',
defaults: {
props: { label: 'Customer ID', placeholder: 'CUST-0001' },
},
},
],
lookupRegistry: {
customerTiers: [
{ label: 'Bronze', value: 'bronze' },
{ label: 'Gold', value: 'gold' },
],
},
validatorPresets: {
input: { minLength: 2 },
},
validatorPresetDefinitions: [
{
id: 'crm-customer-id',
label: 'CRM Customer ID',
fieldKinds: ['input'],
params: [{ key: 'prefix', label: 'Prefix', type: 'string', defaultValue: 'CUST-' }],
resolve: (params) => ({
pattern: `^${String(params['prefix'] ?? 'CUST-')}\\d+$`,
minLength: 6,
}),
},
],
formlyExtensions: [
{
types: [{ name: 'crm-datepicker', component: CrmDatepickerTypeComponent }],
wrappers: [{ name: 'crm-card', component: CrmCardWrapperComponent }],
},
],
};
export const appConfig: ApplicationConfig = {
providers: [{ provide: BUILDER_PLUGINS, useValue: [CRM_PLUGIN] }],
};formlyType on the palette item:{
id: 'crm-date',
category: 'Advanced Fields',
title: 'CRM Date',
nodeType: 'field',
fieldKind: 'input',
formlyType: 'crm-datepicker',
inspectorHint: 'Uses CRM datepicker custom type.',
defaults: { props: { label: 'Date' } },
}validatorPresets are applied when adding fields from palette.validatorPresetDefinitions appear in Inspector > Validation and round-trip in Formly export/import under props.validatorPreset.schemaAdapters appear in the File menu and are merged by adapter id.Load Palette JSONReset Paletteid/category/title/nodeType/defaults.props)childrenTemplate referencesLoad Palette JSON):[
{
"id": "input",
"category": "Common Fields",
"title": "Input",
"nodeType": "field",
"fieldKind": "input",
"defaults": { "props": { "label": "Input", "placeholder": "Enter value" } }
},
{
"id": "textarea",
"category": "Common Fields",
"title": "Textarea",
"nodeType": "field",
"fieldKind": "textarea",
"defaults": { "props": { "label": "Textarea", "placeholder": "Enter details" } }
},
{
"id": "rating",
"category": "Advanced Fields",
"title": "Rating (1-5)",
"nodeType": "field",
"fieldKind": "number",
"defaults": {
"props": { "label": "Rating", "placeholder": "1 to 5" },
"validators": { "min": 1, "max": 5 }
}
},
{
"id": "row",
"category": "Layout",
"title": "Row",
"nodeType": "row",
"defaults": { "props": {}, "childrenTemplate": ["col", "col"] }
},
{
"id": "col",
"category": "Layout",
"title": "Column",
"nodeType": "col",
"defaults": { "props": { "colSpan": 6 } }
},
{
"id": "panel",
"category": "Layout",
"title": "Panel",
"nodeType": "panel",
"defaults": { "props": { "title": "Panel" }, "childrenTemplate": ["row"] }
}
]src/app/app.config.ts):import { BUILDER_PALETTE, type PaletteItem } from './builder-core/registry';
const CUSTOM_PALETTE: PaletteItem[] = [
// Keep ids unique across the list.
// For row/panel templates, childrenTemplate references palette item ids.
];
export const appConfig: ApplicationConfig = {
providers: [{ provide: BUILDER_PALETTE, useValue: CUSTOM_PALETTE }],
};6/6).1..12.row -> col -> row -> col).Add column (row)Rebalance columns (row)Split x2/x3 (column)Advanced Logic Form with predefined examples for:Visible expression / Enabled expression override simple dependsOn rules when present.model, data, and value.model?.status === 'approved'model?.role !== 'readonly' && !!model?.canEditvalid to true, false, or an error-message string.valid = value === 'Joe' ? true : 'Name must be Joe';JSON SchemaOpenAPI 3.0TypeScript InterfaceZod SchemaAngular FormBuilderMinimal example:
Example :import type { BuilderPlugin, BuilderSchemaAdapter } from '@ngx-formly-builder/core';
const MY_API_ADAPTER: BuilderSchemaAdapter = {
id: 'my-api',
label: 'My API Format',
import: (source) => myApiToBuilder(source),
export: (doc) => builderToMyApi(doc),
};
const MY_PLUGIN: BuilderPlugin = {
id: 'my-api-plugin',
schemaAdapters: [MY_API_ADAPTER],
};Recommended rollout:
BuilderDocumentFull guide:
src/public-api.ts exports a stable integration surface for host apps and future packaging.src/app/builder-core/index.ts is the core barrel behind that surface.formly-builder.formly-view.dist/formly-builder (via npm run build:lib).import {
FormlyBuilderComponent,
FormlyViewComponent,
type BuilderDocument,
type BuilderPlugin,
} from '@ngx-formly-builder/core';<formly-builder
[config]="builderConfig"
[plugins]="plugins"
[autosave]="true"
autosaveKey="my-product:builder:draft"
(configChange)="builderConfig = $event"
(diagnosticsChange)="onDiagnostics($event)"
(autosaveError)="onAutosaveError($event)"
/>