@ng-forge/dynamic-form-mcp 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/bin/ng-forge-mcp.js +1126 -478
  2. package/package.json +1 -1
  3. package/src/index.js +1126 -479
  4. package/src/registry/documentation.d.ts +14 -0
  5. package/src/registry/documentation.d.ts.map +1 -0
  6. package/src/registry/field-types.d.ts +9 -0
  7. package/src/registry/field-types.d.ts.map +1 -0
  8. package/src/registry/index.d.ts +10 -16
  9. package/src/registry/index.d.ts.map +1 -1
  10. package/src/registry/instructions.d.ts +1 -1
  11. package/src/registry/instructions.d.ts.map +1 -1
  12. package/src/registry/ui-adapters.d.ts +9 -0
  13. package/src/registry/ui-adapters.d.ts.map +1 -0
  14. package/src/registry/validators.d.ts +9 -0
  15. package/src/registry/validators.d.ts.map +1 -0
  16. package/src/resources/documentation.resource.d.ts +2 -1
  17. package/src/resources/documentation.resource.d.ts.map +1 -1
  18. package/src/services/doc-fetcher.d.ts +34 -0
  19. package/src/services/doc-fetcher.d.ts.map +1 -0
  20. package/src/services/topic-mapper.d.ts +18 -0
  21. package/src/services/topic-mapper.d.ts.map +1 -0
  22. package/src/tools/data/lookup-topics.d.ts +5 -0
  23. package/src/tools/data/lookup-topics.d.ts.map +1 -1
  24. package/src/tools/examples.tool.d.ts +18 -0
  25. package/src/tools/examples.tool.d.ts.map +1 -1
  26. package/src/tools/index.d.ts +8 -6
  27. package/src/tools/index.d.ts.map +1 -1
  28. package/src/tools/lookup.tool.d.ts.map +1 -1
  29. package/src/tools/search.tool.d.ts +9 -0
  30. package/src/tools/search.tool.d.ts.map +1 -0
  31. package/src/tools/validate.tool.d.ts.map +1 -1
  32. package/src/registry/api-reference.json +0 -561
  33. package/src/registry/docs.json +0 -266
  34. package/src/registry/field-types.json +0 -502
  35. package/src/registry/ui-adapters.json +0 -225
  36. package/src/registry/validators.json +0 -210
package/src/index.js CHANGED
@@ -1,287 +1,10 @@
1
- var __getOwnPropNames = Object.getOwnPropertyNames;
2
- var __commonJS = (cb, mod) => function __require() {
3
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
4
- };
5
-
6
- // packages/dynamic-form-mcp/src/registry/docs.json
7
- var require_docs = __commonJS({
8
- "packages/dynamic-form-mcp/src/registry/docs.json"(exports, module) {
9
- module.exports = [
10
- {
11
- id: "advanced-custom-integrations",
12
- title: "Advanced Custom Integrations",
13
- category: "advanced",
14
- content: "> Prerequisites: [Installation](../../installation), [Field Types](../schema-fields/field-types), [Type Safety](../type-safety)\n\nCreate custom UI integrations for ng-forge dynamic forms using any component library or design system.\n\n## Package Structure\n\nThe `@ng-forge/dynamic-forms` package is organized into multiple entrypoints to keep the core abstract and provide specialized utilities for integration authors:\n\n| Entrypoint | Purpose |\n| ------------------------------------- | ---------------------------------------------------------- |\n| `@ng-forge/dynamic-forms` | Core types, components, and configuration (for all users) |\n| `@ng-forge/dynamic-forms/integration` | Field types, mappers, and utilities for UI library authors |\n\nWhen building a custom integration, you'll primarily import from the `/integration` entrypoint:\n\n```typescript\n// Core types (used by everyone)\nimport { DynamicForm, provideDynamicForm, FormConfig, DynamicText } from '@ng-forge/dynamic-forms';\n\n// Integration utilities (for UI library authors)\nimport {\n InputField,\n SelectField,\n CheckboxField,\n valueFieldMapper,\n checkboxFieldMapper,\n createResolvedErrorsSignal,\n} from '@ng-forge/dynamic-forms/integration';\n```\n\n## Integration Overview\n\nUI integrations map field types to your components using `FieldTypeDefinition` objects. Each definition specifies the field type name, component loader, and mapper function.\n\n## Basic Steps\n\n### 1. Define Field Type Interface\n\nCreate a type interface extending the base field type:\n\n```typescript\nimport { ValueFieldComponent, DynamicText } from '@ng-forge/dynamic-forms';\nimport { InputField } from '@ng-forge/dynamic-forms/integration';\n\n// Define your custom props\nexport interface CustomInputProps extends Record<string, unknown> {\n appearance?: 'outline' | 'fill';\n hint?: DynamicText;\n type?: 'text' | 'email' | 'password' | 'number';\n}\n\n// Extend the base InputField with your props\nexport type CustomInputField = InputField<CustomInputProps>;\n\n// Define the component interface (used for type checking)\nexport type CustomInputComponent = ValueFieldComponent<CustomInputField>;\n```\n\n### 2. Create Field Component\n\nImplement the component using Angular's signal forms:\n\n```typescript\nimport { Component, input } from '@angular/core';\nimport { Field, FieldTree } from '@angular/forms/signals';\nimport { DynamicText, DynamicTextPipe } from '@ng-forge/dynamic-forms';\nimport { CustomInputComponent, CustomInputProps } from './custom-input.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'custom-input',\n imports: [Field, DynamicTextPipe, AsyncPipe],\n template: `\n @let f = field();\n\n <div class=\"custom-field\" [class.custom-outline]=\"props()?.appearance === 'outline'\">\n @if (label()) {\n <label [for]=\"key() + '-input'\">{% raw %}{{ label() | dynamicText | async }}{% endraw %}</label>\n }\n\n <input\n [id]=\"key() + '-input'\"\n [field]=\"f\"\n [type]=\"props()?.type || 'text'\"\n [placeholder]=\"{% raw %}(placeholder() | dynamicText | async) ?? ''{% endraw %}\"\n [disabled]=\"f().disabled()\"\n [attr.tabindex]=\"tabIndex()\"\n />\n\n @if (props()?.hint; as hint) {\n <div class=\"hint\">{% raw %}{{ hint | dynamicText | async }}{% endraw %}</div>\n } @if (f().touched() && f().invalid()) {\n <div class=\"error\">{% raw %}{{ f().errors() | json }}{% endraw %}</div>\n }\n </div>\n `,\n host: {\n '[id]': '`${key()}`',\n '[class]': 'className()',\n },\n})\nexport default class CustomInputFieldComponent implements CustomInputComponent {\n // Required inputs\n readonly field = input.required<FieldTree<string>>();\n readonly key = input.required<string>();\n\n // Standard inputs\n readonly label = input<DynamicText>();\n readonly placeholder = input<DynamicText>();\n readonly className = input<string>('');\n readonly tabIndex = input<number>();\n\n // Custom props\n readonly props = input<CustomInputProps>();\n}\n```\n\n### 3. Create Field Type Definition\n\nDefine the field type registration:\n\n```typescript\nimport { FieldTypeDefinition } from '@ng-forge/dynamic-forms';\nimport { valueFieldMapper } from '@ng-forge/dynamic-forms/integration';\n\nexport const CustomInputType: FieldTypeDefinition = {\n name: 'input',\n loadComponent: () => import('./custom-input.component'),\n mapper: valueFieldMapper,\n};\n```\n\n### 4. Create Provider Function\n\nExport a function that returns all your field type definitions:\n\n```typescript\nimport { FieldTypeDefinition } from '@ng-forge/dynamic-forms';\nimport { CustomInputType } from './fields/input';\nimport { CustomSelectType } from './fields/select';\nimport { CustomCheckboxType } from './fields/checkbox';\n\nexport function withCustomFields(): FieldTypeDefinition[] {\n return [\n CustomInputType,\n CustomSelectType,\n CustomCheckboxType,\n // ... more field types\n ];\n}\n```\n\n### 5. Configure App\n\nAdd your fields to the app configuration:\n\n```typescript name=\"app.config.ts\"\nimport { ApplicationConfig } from '@angular/core';\nimport { provideDynamicForm } from '@ng-forge/dynamic-forms';\nimport { withCustomFields } from './custom-fields';\n\nexport const appConfig: ApplicationConfig = {\n providers: [provideDynamicForm(...withCustomFields())],\n};\n```\n\n## Component Interface Types\n\nng-forge provides component interface types for different field categories:\n\n### ValueFieldComponent\n\nFor fields that collect user input (input, select, textarea, datepicker, radio, slider):\n\n```typescript\nimport { ValueFieldComponent } from '@ng-forge/dynamic-forms';\nimport { InputField } from '@ng-forge/dynamic-forms/integration';\n\nexport type CustomInputComponent = ValueFieldComponent<CustomInputField>;\n```\n\nThe component must implement these inputs:\n\n- `field: FieldTree<TValue>` - The form field from Angular's signal forms\n- `key: string` - Unique field identifier\n- `label?: DynamicText` - Field label\n- `placeholder?: DynamicText` - Placeholder text\n- `className?: string` - CSS classes\n- `tabIndex?: number` - Tab order\n- `props?: TProps` - Custom field-specific props\n- `meta?: FieldMeta` - Native HTML attributes (data-_, aria-_, autocomplete, etc.)\n\n### CheckedFieldComponent\n\nFor checkbox and toggle fields:\n\n```typescript\nimport { CheckedFieldComponent } from '@ng-forge/dynamic-forms';\nimport { CheckboxField } from '@ng-forge/dynamic-forms/integration';\n\nexport type CustomCheckboxComponent = CheckedFieldComponent<CustomCheckboxField>;\n```\n\nSimilar to ValueFieldComponent but specifically for boolean checkbox fields.\n\n## Field Binding with [field]\n\nThe key to connecting your component to Angular's signal forms is the `[field]` binding. Import `Field` and `FieldTree` from Angular's signal forms package:\n\n```typescript\nimport { Field, FieldTree } from '@angular/forms/signals';\n```\n\nThen use the `[field]` directive on form controls:\n\n```typescript\n<input [field]=\"f\" ... />\n<mat-checkbox [field]=\"f\" ... />\n<select [field]=\"f\" ... />\n```\n\nThis directive automatically:\n\n- Binds the form control value\n- Handles value changes\n- Manages validation state\n- Syncs disabled/readonly states\n\n## Field Mappers\n\nMappers convert field definitions to component input bindings. ng-forge provides built-in mappers:\n\n### valueFieldMapper\n\nFor standard value-bearing fields:\n\n```typescript\nimport { FieldTypeDefinition } from '@ng-forge/dynamic-forms';\nimport { valueFieldMapper } from '@ng-forge/dynamic-forms/integration';\n\nexport const CustomInputType: FieldTypeDefinition = {\n name: 'input',\n loadComponent: () => import('./custom-input.component'),\n mapper: valueFieldMapper, // Maps value fields\n};\n```\n\n### checkboxFieldMapper\n\nFor checkbox/toggle fields:\n\n```typescript\nimport { FieldTypeDefinition } from '@ng-forge/dynamic-forms';\nimport { checkboxFieldMapper } from '@ng-forge/dynamic-forms/integration';\n\nexport const CustomCheckboxType: FieldTypeDefinition = {\n name: 'checkbox',\n loadComponent: () => import('./custom-checkbox.component'),\n mapper: checkboxFieldMapper, // Maps checkbox fields\n};\n```\n\n### Custom Mappers\n\nFor specialized fields (like buttons), create custom mappers:\n\n```typescript\nimport { Binding, inputBinding } from '@angular/core';\nimport { FieldTypeDefinition } from '@ng-forge/dynamic-forms';\nimport { ButtonField } from '@ng-forge/dynamic-forms/integration';\n\nexport function buttonFieldMapper(fieldDef: ButtonField<unknown, unknown>): Binding[] {\n return [\n inputBinding('key', () => fieldDef.key),\n inputBinding('label', () => fieldDef.label),\n inputBinding('disabled', () => fieldDef.disabled ?? false),\n inputBinding('event', () => fieldDef.event),\n inputBinding('props', () => fieldDef.props),\n inputBinding('className', () => fieldDef.className),\n ];\n}\n\nexport const CustomButtonType: FieldTypeDefinition = {\n name: 'button',\n loadComponent: () => import('./custom-button.component'),\n mapper: buttonFieldMapper,\n valueHandling: 'exclude', // Buttons don't contribute to form value\n};\n```\n\n## Value Handling\n\nThe `valueHandling` property controls whether a field contributes to the form value:\n\n- `'include'` (default) - Field value included in form data\n- `'exclude'` - Field excluded from form data (for buttons, text fields, etc.)\n\n```typescript\nexport const ButtonType: FieldTypeDefinition = {\n name: 'button',\n loadComponent: () => import('./button.component'),\n mapper: buttonFieldMapper,\n valueHandling: 'exclude', // Buttons don't have values\n};\n```\n\n## Type Safety with Module Augmentation\n\nRegister your field types with TypeScript for full type inference:\n\n```typescript\n// In your field types file\ndeclare module '@ng-forge/dynamic-forms' {\n interface FieldRegistryLeaves {\n input: CustomInputField;\n select: CustomSelectField;\n checkbox: CustomCheckboxField;\n }\n}\n```\n\nThis enables:\n\n- IntelliSense for field properties\n- Type checking in form configurations\n- Compile-time validation of field definitions\n\n## Handling Meta Attributes\n\n`meta` attributes are native HTML attributes that should be applied to the underlying form element. They differ from `props` (which control UI library behavior). See [Props vs Meta](../schema-fields/field-types#props-vs-meta) for detailed usage guidance.\n\n### Props vs Meta Summary\n\n| Attribute Type | Example | Use `props` | Use `meta` |\n| ------------------ | ----------------------- | ----------- | ---------- |\n| UI appearance | `appearance: 'outline'` | \u2705 | \u274C |\n| Component behavior | `multiple: true` | \u2705 | \u274C |\n| Browser autofill | `autocomplete: 'email'` | \u274C | \u2705 |\n| Testing IDs | `data-testid: 'email'` | \u274C | \u2705 |\n| Accessibility | `aria-describedby` | \u274C | \u2705 |\n\n### Using setupMetaTracking\n\nng-forge provides the `setupMetaTracking` utility to apply meta attributes to native elements. This uses Angular's `afterRenderEffect` for efficient DOM updates.\n\n```typescript\nimport { Component, ElementRef, inject, input } from '@angular/core';\nimport { FieldMeta, setupMetaTracking } from '@ng-forge/dynamic-forms/integration';\n\n@Component({\n template: ` <input [field]=\"f\" /> `,\n})\nexport default class CustomInputComponent {\n private readonly elementRef = inject(ElementRef<HTMLElement>);\n readonly meta = input<FieldMeta>();\n\n constructor() {\n // Apply meta attributes to the native input element\n setupMetaTracking(this.elementRef, this.meta, { selector: 'input' });\n }\n}\n```\n\n**Parameters:**\n\n- `elementRef`: Reference to the host element\n- `meta`: Signal containing the meta attributes\n- `options.selector`: CSS selector to find the target element(s) within the host\n\n### Components with Dynamic Options\n\nFor components with dynamic options (radio groups, multi-checkbox), pass a `dependents` array to ensure meta updates when options change:\n\n```typescript\n@Component({\n template: `\n @for (option of options(); track option.value) {\n <label>\n <input type=\"radio\" [value]=\"option.value\" />\n {% raw %}{{ option.label }}{% endraw %}\n </label>\n }\n `,\n})\nexport default class CustomRadioComponent {\n private readonly elementRef = inject(ElementRef<HTMLElement>);\n readonly meta = input<FieldMeta>();\n readonly options = input<Option[]>([]);\n\n constructor() {\n // Re-apply meta when options change (new inputs are rendered)\n setupMetaTracking(this.elementRef, this.meta, {\n selector: 'input[type=\"radio\"]',\n dependents: [this.options],\n });\n }\n}\n```\n\n### Shadow DOM Considerations\n\nFor components using Shadow DOM (like Ionic), you cannot access the internal input. Apply meta to the host element by omitting the `selector`:\n\n```typescript\n@Component({\n template: `\n <ion-checkbox [checked]=\"value()\">\n {% raw %}{{ label() }}{% endraw %}\n </ion-checkbox>\n `,\n})\nexport default class IonicCheckboxComponent {\n private readonly elementRef = inject(ElementRef<HTMLElement>);\n readonly meta = input<FieldMeta>();\n\n constructor() {\n // No selector: applies meta to the host element itself\n setupMetaTracking(this.elementRef, this.meta);\n }\n}\n```\n\n## Best Practices\n\n**Use proper component interfaces:**\n\n- Implement `ValueFieldComponent<T>` for value fields\n- Implement `CheckedFieldComponent<T>` for checkboxes/toggles\n- Define clear prop interfaces\n\n**Handle meta attributes:**\n\n- All components must accept a `meta` input\n- Use `setupMetaTracking` with a selector for native elements\n- Pass `dependents` for components with dynamic options\n- Omit selector for Shadow DOM components (applies to host)\n\n**Leverage [field] binding:**\n\n- Use `[field]=\"f\"` on form controls\n- Automatic value and validation handling\n- No manual form control management needed\n\n**Support DynamicText:**\n\n- Accept `DynamicText` for labels, hints, placeholders\n- Use `DynamicTextPipe` for rendering\n- Enables i18n with any translation library\n\n**Handle validation state:**\n\n- Show errors when `f().touched() && f().invalid()`\n- Display validation messages from `f().errors()`\n- Clear, accessible error presentation\n\n**Accessibility:**\n\n- Proper ARIA attributes\n- Keyboard navigation\n- Focus management\n- Screen reader support\n\n**Lazy loading:**\n\n- Use dynamic imports in `loadComponent`\n- Keeps initial bundle size small\n- Components load on-demand\n\n## Reference Implementations\n\nSee complete integrations:\n\n- [Material Design](../../ui-libs-integrations/material) - Full Material implementation with 12+ field types\n- [Bootstrap](../../ui-libs-integrations/bootstrap)\n- [PrimeNG](../../ui-libs-integrations/primeng)\n- [Ionic](../../ui-libs-integrations/ionic)\n\nThe Material integration source code is the most comprehensive example of implementing custom field types.\n\n## Related Topics\n\n- **[Material Integration](../../ui-libs-integrations/material)** - Complete reference implementation\n- **[Field Types](../schema-fields/field-types)** - Understanding all available field types\n- **[Type Safety](../type-safety)** - Module augmentation for custom types\n- **[Validation](../validation/basics)** - Displaying validation errors in custom fields"
15
- },
16
- {
17
- id: "advanced-events",
18
- title: "Advanced Events",
19
- category: "advanced",
20
- content: "The event bus enables communication between the `dynamic-form` component and custom field components within it. Each form instance has its own isolated event bus.\n\n## Overview\n\nThe event bus provides:\n\n- Communication between field components within a single form\n- Type-safe event subscriptions\n- Reactive event handling with RxJS\n- Wizard page navigation coordination\n- Custom field component workflows\n\n**Note**: The event bus is scoped to each `dynamic-form` instance - events don't cross form boundaries.\n\n## Usage in Custom Field Components\n\nWhen building custom field types, inject the event bus to communicate with the parent form or other fields:\n\n```typescript\nimport { Component, inject } from '@angular/core';\nimport { EventBus } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-custom-submit-button',\n template: `<button (click)=\"submit()\">Submit Form</button>`,\n})\nexport class CustomSubmitButton {\n eventBus = inject(EventBus);\n\n submit() {\n // Dispatch submit event to the parent dynamic-forms\n this.eventBus.dispatch(SubmitEvent);\n }\n}\n```\n\n## Built-in Events\n\n### SubmitEvent\n\nFired when form is submitted.\n\n```typescript\neventBus.on<SubmitEvent>('submit').subscribe(() => {\n // Handle form submission\n console.log('Form submitted');\n});\n```\n\n### PageChangeEvent\n\nFired when navigating between wizard pages.\n\n```typescript\neventBus.on<PageChangeEvent>('page-change').subscribe((event) => {\n console.log(`Page ${event.currentPageIndex + 1} of ${event.totalPages}`);\n console.log(`Previous page: ${event.previousPageIndex}`);\n});\n```\n\n**Properties:**\n\n- `currentPageIndex: number` - Current page (0-based)\n- `totalPages: number` - Total number of pages\n- `previousPageIndex?: number` - Previous page index\n\n### NextPageEvent\n\nNavigate to next page in wizard.\n\n```typescript\neventBus.dispatch(NextPageEvent);\n```\n\n### PreviousPageEvent\n\nNavigate to previous page in wizard.\n\n```typescript\neventBus.dispatch(PreviousPageEvent);\n```\n\n### FormResetEvent\n\nReset form to default values.\n\n```typescript\n// Dispatch reset event\neventBus.dispatch(FormResetEvent);\n\n// Listen for reset\neventBus.on<FormResetEvent>('form-reset').subscribe(() => {\n console.log('Form was reset to defaults');\n});\n```\n\n### FormClearEvent\n\nClear all form values (empty state, not defaults).\n\n```typescript\n// Dispatch clear event\neventBus.dispatch(FormClearEvent);\n\n// Listen for clear\neventBus.on<FormClearEvent>('form-clear').subscribe(() => {\n console.log('Form was cleared');\n});\n```\n\n### Array Events\n\nThe `arrayEvent()` builder provides a fluent API for array field manipulation. Import it from the main package:\n\n```typescript\nimport { arrayEvent } from '@ng-forge/dynamic-forms';\n```\n\n#### Adding Items\n\n**Important:** A template is required for all add operations. The template defines the structure of the new item:\n\n- **Single FieldDef** \u2192 creates a **primitive item** (field value extracted directly)\n- **Array of FieldDefs** \u2192 creates an **object item** (fields merged into object)\n\n```typescript\n// Define templates\nconst tagTemplate = { key: 'tag', type: 'input', label: 'Tag' }; // Primitive item\nconst contactTemplate = [\n // Object item\n { key: 'name', type: 'input', label: 'Name' },\n { key: 'email', type: 'input', label: 'Email' },\n];\n\n// Append item at end (most common)\neventBus.dispatch(arrayEvent('tags').append(tagTemplate)); // Primitive: adds a string value\neventBus.dispatch(arrayEvent('contacts').append(contactTemplate)); // Object: adds { name, email }\n\n// Prepend item at beginning\neventBus.dispatch(arrayEvent('contacts').prepend(contactTemplate));\n\n// Insert at specific index\neventBus.dispatch(arrayEvent('contacts').insertAt(2, contactTemplate));\n```\n\n#### Removing Items\n\n```typescript\n// Remove last item (stack pop)\neventBus.dispatch(arrayEvent('contacts').pop());\n\n// Remove first item (queue shift)\neventBus.dispatch(arrayEvent('contacts').shift());\n\n// Remove item at specific index\neventBus.dispatch(arrayEvent('contacts').removeAt(2));\n```\n\n#### Internal Event Classes\n\nFor advanced use cases (extending events or type-checking), you can import the underlying event classes:\n\n```typescript\nimport {\n AppendArrayItemEvent,\n PrependArrayItemEvent,\n InsertArrayItemEvent,\n PopArrayItemEvent,\n ShiftArrayItemEvent,\n RemoveAtIndexEvent,\n} from '@ng-forge/dynamic-forms';\n\n// Example: extend for custom template\nexport class AddContactEvent extends AppendArrayItemEvent {\n constructor() {\n super('contacts', [\n { key: 'name', type: 'input', label: 'Name' },\n { key: 'phone', type: 'input', label: 'Phone' },\n ]);\n }\n}\n\n// Usage\neventBus.dispatch(new AddContactEvent());\n```\n\n**Event types:**\n\n| Event | Description |\n| ----------------------- | ------------------------------ |\n| `AppendArrayItemEvent` | Add item at end of array |\n| `PrependArrayItemEvent` | Add item at beginning of array |\n| `InsertArrayItemEvent` | Add item at specific index |\n| `PopArrayItemEvent` | Remove last item |\n| `ShiftArrayItemEvent` | Remove first item |\n| `RemoveAtIndexEvent` | Remove item at specific index |\n\n## Multiple Event Subscriptions\n\nSubscribe to multiple event types by passing an array of type strings:\n\n```typescript\neventBus.on<SubmitEvent | PageChangeEvent | NextPageEvent>(['submit', 'page-change', 'next-page']).subscribe((event) => {\n switch (event.type) {\n case 'submit':\n handleSubmit();\n break;\n case 'page-change':\n handlePageChange(event);\n break;\n case 'next-page':\n handleNextPage();\n break;\n }\n});\n```\n\n## Custom Events\n\nCreate custom events for your forms:\n\n```typescript\nimport { FormEvent } from '@ng-forge/dynamic-forms';\n\n// Simple event\nexport class SaveDraftEvent implements FormEvent {\n readonly type = 'save-draft' as const;\n}\n\n// Event with payload\nexport class ValidationErrorEvent implements FormEvent {\n readonly type = 'validation-error' as const;\n\n constructor(\n public readonly fieldKey: string,\n public readonly errorMessage: string,\n ) {}\n}\n```\n\nUsage:\n\n```typescript\n// Dispatch simple event\neventBus.dispatch(SaveDraftEvent);\n\n// Dispatch event with payload\neventBus.dispatch(ValidationErrorEvent, 'email', 'Invalid email format');\n\n// Subscribe to custom event\neventBus.on<SaveDraftEvent>('save-draft').subscribe(() => {\n saveDraft();\n});\n\neventBus.on<ValidationErrorEvent>('validation-error').subscribe((event) => {\n showError(event.fieldKey, event.errorMessage);\n});\n```\n\n## Practical Examples\n\n### Form Auto-save\n\n```typescript\n@Component({...})\nexport class AutoSaveFormComponent {\n eventBus = inject(EventBus);\n\n ngOnInit() {\n // Auto-save on page change\n this.eventBus.on<PageChangeEvent>('page-change').subscribe(() => {\n this.saveDraft();\n });\n }\n\n saveDraft() {\n // Save form data\n }\n}\n```\n\n### Multi-step Form Navigation\n\n```typescript\n@Component({\n template: `\n <button (click)=\"previous()\">Previous</button>\n <button (click)=\"next()\">Next</button>\n <button (click)=\"submit()\">Submit</button>\n `,\n})\nexport class WizardNavigationComponent {\n eventBus = inject(EventBus);\n\n previous() {\n this.eventBus.dispatch(PreviousPageEvent);\n }\n\n next() {\n this.eventBus.dispatch(NextPageEvent);\n }\n\n submit() {\n this.eventBus.dispatch(SubmitEvent);\n }\n}\n```\n\n### Progress Tracking\n\n```typescript\n@Component({...})\nexport class ProgressTrackerComponent {\n eventBus = inject(EventBus);\n progress = signal(0);\n\n ngOnInit() {\n this.eventBus.on<PageChangeEvent>('page-change').subscribe((event) => {\n const percentage = ((event.currentPageIndex + 1) / event.totalPages) * 100;\n this.progress.set(percentage);\n });\n }\n}\n```\n\n### Form Reset and Clear\n\n```typescript\n@Component({...})\nexport class FormActionsComponent {\n eventBus = inject(EventBus);\n\n resetForm() {\n // Reset form to default values\n this.eventBus.dispatch(FormResetEvent);\n }\n\n clearForm() {\n // Clear all form values\n this.eventBus.dispatch(FormClearEvent);\n }\n}\n```\n\n## Best Practices\n\n**Use built-in events:**\n\n- Leverage existing events for common scenarios\n- Consistent behavior across forms\n\n**Create custom events for domain logic:**\n\n- Keep event names descriptive\n- Include necessary payload data\n- Use readonly properties\n\n**Type safety:**\n\n```typescript\n// \u2713 Type-safe event subscription\nclass UserUpdatedEvent implements FormEvent {\n readonly type = 'user-updated' as const;\n constructor(public readonly userId: string) {}\n}\n\n// \u2713 Use generic parameter for type inference\neventBus.on<UserUpdatedEvent>('user-updated').subscribe((event) => {\n console.log(event.userId); // TypeScript knows event has userId\n});\n\n// \u2717 Avoid subscriptions without generic type parameter\neventBus.on('some-random-event'); // No type checking on event properties\n```\n\n## Attaching Form Values to Events\n\nBy default, events don't include the form's current value. You can opt-in to automatically attach the form value to all dispatched events using `withEventFormValue()`.\n\n### Global Opt-in\n\nEnable for all forms in your application:\n\n```typescript\nimport { provideDynamicForm, withEventFormValue } from '@ng-forge/dynamic-forms';\n\nprovideDynamicForm(...withMaterialFields(), withEventFormValue());\n```\n\n### Per-Form Control\n\nOverride the global setting for specific forms:\n\n```typescript\nconst config: FormConfig = {\n fields: [...],\n options: {\n // Enable for this form (even if globally disabled)\n emitFormValueOnEvents: true,\n // Or disable for this form (even if globally enabled)\n // emitFormValueOnEvents: false,\n }\n};\n```\n\n### Accessing the Form Value\n\nUse the `hasFormValue()` type guard to safely access the attached value:\n\n```typescript\nimport { hasFormValue } from '@ng-forge/dynamic-forms';\n\neventBus.on<SubmitEvent>('submit').subscribe((event) => {\n if (hasFormValue(event)) {\n // TypeScript knows event.formValue exists\n console.log('Form value at submission:', event.formValue);\n sendToApi(event.formValue);\n }\n});\n```\n\nThis is useful when you need the complete form state at the time an event occurred, such as:\n\n- Logging form values on submission\n- Auto-saving on page changes\n- Analytics tracking\n\n## Event Flow\n\n1. Component dispatches event via `eventBus.dispatch()`\n2. Event bus broadcasts to all subscribers\n3. Subscribers receive event and execute handlers\n4. Type filtering ensures only relevant handlers execute\n\n## Integration with Forms\n\nThe event bus is automatically provided by the `DynamicForm` component to all child field components. You don't need to manually provide it:\n\n```typescript\n// The DynamicForm component internally provides EventBus\n// All child field components can inject and use it\nimport { DynamicForm } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-my-form',\n imports: [DynamicForm],\n template: `<form [dynamic-form]=\"config\"></form>`,\n})\nexport class MyFormComponent {\n // EventBus is available to all field components rendered by DynamicForm\n}\n```\n\nField components can inject and use the event bus:\n\n```typescript\nexport class CustomFieldComponent extends BaseFieldComponent {\n eventBus = inject(EventBus);\n\n onSubmit() {\n // Trigger form submission from custom component\n this.eventBus.dispatch(SubmitEvent);\n }\n}\n```"
21
- },
22
- {
23
- id: "advanced-expression-parser",
24
- title: "Advanced Expression Parser",
25
- category: "advanced",
26
- content: "Dynamic forms use expressions for conditional logic and dynamic values. When you use JavaScript expressions with `type: 'javascript'`, the expression parser evaluates them safely, preventing code injection attacks while maintaining the flexibility you need.\n\n## How JavaScript Expressions Work\n\nWhen you configure JavaScript expressions in your form:\n\n```typescript\n{\n key: 'driverLicense',\n type: 'input',\n value: '',\n label: 'Driver License',\n logic: [\n {\n type: 'required',\n condition: {\n type: 'javascript',\n expression: 'formValue.age >= 18 && formValue.needsTransport === true'\n },\n }\n ]\n}\n```\n\nThe expression `'formValue.age >= 18 && formValue.needsTransport === true'` is evaluated by the parser, which has access to:\n\n```typescript\n{\n fieldValue: currentFieldValue,\n formValue: { age: 25, needsTransport: true, ... },\n fieldPath: 'driverLicense'\n}\n```\n\n## Security Model\n\nThe parser uses different rules for methods and properties:\n\n### Methods: Whitelist Only\n\nOnly safe methods can be called in expressions:\n\n```typescript\n// \u2705 Works - safe string methods\n{\n type: 'hidden',\n condition: {\n type: 'javascript',\n expression: 'formValue.email.includes(\"@company.com\")'\n }\n}\n\n// \u2705 Works - safe array methods\n{\n type: 'hidden',\n condition: {\n type: 'javascript',\n expression: 'formValue.roles.some((role) => role === \"admin\")'\n }\n}\n\n// \u274C Blocked - not whitelisted\n{\n type: 'hidden',\n condition: {\n type: 'javascript',\n expression: 'formValue.text.link(\"url\")' // Error: Method \"link\" is not allowed\n }\n}\n```\n\n### Whitelisted Methods Reference\n\n| Category | Methods |\n| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **String** | `toUpperCase`, `toLowerCase`, `trim`, `includes`, `startsWith`, `endsWith`, `slice`, `substring`, `charAt`, `indexOf`, `lastIndexOf`, `split`, `replace`, `match` |\n| **Array** | `map`, `filter`, `some`, `every`, `find`, `findIndex`, `includes`, `indexOf`, `join`, `slice`, `flat`, `flatMap` |\n| **Number** | `toFixed`, `toString`, `valueOf` |\n| **Object** | `hasOwnProperty`, `toString`, `valueOf` |\n\n### Properties: Open Access (Except Dangerous Ones)\n\nAll properties in the form data are accessible, except prototype properties:\n\n```typescript\n// \u2705 Works - access any form field\nexpression: 'formValue.firstName';\nexpression: 'formValue._internalFlag';\nexpression: 'formValue.nested.deeply.property';\n\n// \u274C Blocked - prototype pollution risks\nexpression: 'formValue.constructor'; // Error: Property \"constructor\" is not accessible\nexpression: 'formValue.__proto__'; // Error: Property \"__proto__\" is not accessible\n```\n\n**Why?** Dynamic forms need to access any field name you define. The parser blocks only the dangerous properties that could break security.\n\n### Safe Member Access (Built-in)\n\n**Important**: Member access is safe by default - accessing properties on `null` or `undefined` returns `undefined` instead of throwing errors:\n\n```typescript\n// \u2705 All of these work safely, even when intermediate values are null/undefined\nexpression: 'formValue.user.profile.firstName';\nexpression: 'formValue.address.city.toLowerCase()';\nexpression: 'formValue.settings.notifications.email';\n\n// If formValue.user is null, the expression returns undefined (no error thrown)\n// If formValue.user.profile is null, the expression returns undefined (no error thrown)\n// No manual null checks needed!\n```\n\n**This means you don't need to write defensive null checks:**\n\n```typescript\n// \u274C Unnecessary - Don't do this\nexpression: '!formValue.user || !formValue.user.profile || !formValue.user.profile.firstName || fieldValue !== formValue.user.profile.firstName';\n\n// \u2705 Better - Safe by default\nexpression: '!formValue.user.profile.firstName || fieldValue !== formValue.user.profile.firstName';\n```\n\nThe parser automatically handles null/undefined at every level of property access, making expressions cleaner and more maintainable.\n\n## Available Context Variables\n\nWhen expressions are evaluated, they have access to:\n\n```typescript\n{\n fieldValue: 'current field value',\n formValue: { /* entire form state */ },\n fieldPath: 'fieldName',\n externalData: { /* external application state, if configured */ }\n}\n```\n\n### External Data\n\nWhen you configure `externalData` in your FormConfig, it becomes available in expressions:\n\n```typescript\nconst config = {\n externalData: {\n userRole: computed(() => authService.role()),\n permissions: computed(() => authService.permissions()),\n },\n fields: [\n {\n key: 'adminField',\n type: 'input',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'javascript',\n expression: \"externalData.userRole !== 'admin'\",\n },\n },\n ],\n },\n ],\n} as const satisfies FormConfig;\n```\n\n## Common Form Use Cases\n\n### Dynamic Visibility\n\n```typescript\n{\n key: 'companyName',\n type: 'input',\n value: '',\n label: 'Company Name',\n // Show only if user selects \"Employed\"\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'javascript',\n expression: 'formValue.employmentStatus !== \"employed\"'\n }\n }\n ]\n}\n```\n\n### Conditional Required\n\n```typescript\n{\n key: 'taxId',\n type: 'input',\n value: '',\n label: 'Tax ID',\n // Required only for business accounts\n logic: [\n {\n type: 'required',\n condition: {\n type: 'javascript',\n expression: 'formValue.accountType === \"business\"'\n },\n }\n ]\n}\n```\n\n### Complex Conditions\n\n```typescript\n{\n key: 'personalConcierge',\n type: 'checkbox',\n value: false,\n label: 'Request Personal Concierge Service',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'javascript',\n expression: 'formValue.membershipLevel !== \"vip\" || formValue.annualIncome < 100000'\n }\n }\n ]\n}\n```\n\n### String Methods in Expressions\n\n```typescript\n{\n key: 'discountCode',\n type: 'input',\n value: '',\n label: 'Discount Code',\n logic: [\n {\n type: 'required',\n condition: {\n type: 'javascript',\n expression: 'formValue.email.endsWith(\"@company.com\")'\n },\n }\n ]\n}\n```\n\n## What the Parser Prevents\n\nFor dynamic forms, the parser prevents:\n\n- \u2705 **Code Injection**: Can't execute arbitrary code or create new code\n- \u2705 **Prototype Pollution**: Can't access `constructor` or `__proto__`\n- \u2705 **Unsafe Operations**: Can't call methods that modify state or access globals\n\n## Custom Functions\n\nWhen providing custom functions for use in expressions, register them with the FunctionRegistryService:\n\n```typescript\n// \u2705 GOOD - Pure functions\nimport { FunctionRegistryService } from '@ng-forge/dynamic-forms';\n\nconst functionRegistry = inject(FunctionRegistryService);\n\nfunctionRegistry.registerCustomFunction('isValidEmail', (ctx) => {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(ctx.fieldValue);\n});\n\nfunctionRegistry.registerCustomFunction('isAdult', (ctx) => {\n return ctx.formValue.age >= 18;\n});\n```\n\nThen use them with `type: 'custom'`:\n\n```typescript\n{\n key: 'adultContent',\n type: 'checkbox',\n value: false,\n label: 'Access Adult Content',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'custom',\n expression: 'isAdult'\n }\n }\n ]\n}\n```\n\n**Important**: Only provide pure functions without side effects:\n\n```typescript\n// \u274C BAD - Side effects\nfunctionRegistry.registerCustomFunction('logValue', (ctx) => {\n console.log(ctx.fieldValue); // Side effect!\n trackAnalytics(ctx); // Side effect!\n return true;\n});\n```\n\n## Supported Expression Features\n\nIn JavaScript expressions (`type: 'javascript'`), you can use:\n\n| Feature | Examples |\n| --------------------- | --------------------------------------------------------------- |\n| **Property access** | `formValue.name`, `formValue.user.profile.email` |\n| **Comparisons** | `===`, `!==`, `>`, `<`, `>=`, `<=` |\n| **Logical operators** | `&&`, `\\|\\|`, `!` |\n| **Arithmetic** | `+`, `-`, `*`, `/`, `%` |\n| **String methods** | See [Whitelisted Methods](#whitelisted-methods-reference) above |\n| **Array methods** | See [Whitelisted Methods](#whitelisted-methods-reference) above |\n\n**Example**:\n\n```typescript\nexpression: 'formValue.age >= 18 && formValue.email.includes(\"@example.com\")';\n```\n\n**Not Supported**: Object literals `{}`, ternary `a ? b : c`, assignment `x = 5`, `new` keyword, function declarations\n\n## Summary\n\nThe expression parser lets you write flexible conditional logic while preventing code injection attacks:\n\n1. **Form state is accessible** - Any field in `formValue` can be read\n2. **Methods are restricted** - Only safe, non-mutating methods allowed\n3. **Prototype is protected** - Can't access dangerous properties\n4. **Custom functions supported** - Register pure functions for reusable logic"
27
- },
28
- {
29
- id: "advanced-type-safety-basics",
30
- title: "Advanced Type Safety Basics",
31
- category: "advanced",
32
- content: "Complete type inference for form configurations using TypeScript's type system and Angular signal forms.\n\n## Overview\n\nDynamic Forms provides compile-time type inference for form configurations, eliminating manual type definitions and catching errors before runtime. All form values are automatically inferred from your configuration using TypeScript's advanced type features.\n\n## Basic Type Inference\n\nUse `as const satisfies FormConfig` to enable type inference:\n\n```typescript\nimport { FormConfig } from '@ng-forge/dynamic-forms';\n\nconst formConfig = {\n fields: [\n { key: 'firstName', type: 'input', value: '' },\n { key: 'email', type: 'input', value: '' },\n { key: 'age', type: 'input', value: 0, props: { type: 'number' } },\n { key: 'newsletter', type: 'checkbox', value: false },\n ],\n} as const satisfies FormConfig;\n\n// Type is automatically inferred based on value property:\n// { firstName?: string; email?: string; age?: number; newsletter?: boolean }\n```\n\n**How it works:**\n\n1. `as const` makes the configuration deeply readonly, preserving literal types\n2. `satisfies FormConfig` validates the config structure without widening types\n3. TypeScript infers the form value type from the `value` properties\n\n## The `value` Property\n\nThe `value` property is critical for type inference - its type determines the inferred field type:\n\n```typescript\nconst config = {\n fields: [\n { key: 'name', type: 'input', value: '' }, // string\n { key: 'age', type: 'input', value: 0 }, // number\n { key: 'active', type: 'checkbox', value: false }, // boolean\n { key: 'tags', type: 'multi-checkbox', value: [] }, // string[]\n ],\n} as const satisfies FormConfig;\n\n// Inferred type:\n// {\n// name?: string;\n// age?: number;\n// active?: boolean;\n// tags?: string[];\n// }\n```\n\n## Field Registry System\n\nUI integrations register their field types via `provideDynamicForm`. For example, with Material Design:\n\n```typescript\n// app.config.ts\nimport { provideDynamicForm } from '@ng-forge/dynamic-forms';\nimport { withMaterialFields } from '@ng-forge/dynamic-forms-material';\n\nexport const appConfig: ApplicationConfig = {\n providers: [\n provideDynamicForm(...withMaterialFields()),\n // other providers\n ],\n};\n```\n\nMaterial fields augment the `FieldRegistryLeaves` interface for type safety:\n\n```typescript\n// From @ng-forge/dynamic-forms-material (already included)\ndeclare module '@ng-forge/dynamic-forms' {\n interface FieldRegistryLeaves {\n input: MatInputField;\n select: MatSelectField<unknown>;\n checkbox: MatCheckboxField;\n // ... other Material fields\n }\n}\n```\n\nThis determines:\n\n- What field `type` values are valid (e.g., `'input'`, `'select'`)\n- How each field type is structured\n- What props are available for each field\n\n**Example - Wrong field type:**\n\n```typescript\nconst config = {\n fields: [\n { key: 'name', type: 'invalid', value: '' }, // TypeScript error!\n ],\n} as const satisfies FormConfig;\n```\n\n## Required vs Optional Fields\n\nThe `required` flag affects whether fields include `undefined` in their type:\n\n```typescript\nconst config = {\n fields: [\n { key: 'email', type: 'input', value: '', required: true }, // string (required)\n { key: 'name', type: 'input', value: '' }, // string | undefined\n { key: 'age', type: 'input', value: 0, required: false }, // number | undefined\n ],\n} as const satisfies FormConfig;\n\n// Inferred type:\n// {\n// email: string; // Required - no undefined\n// name?: string; // Optional - includes undefined\n// age?: number; // Optional - includes undefined\n// }\n```\n\n**In practice:**\n\n```typescript\nfunction handleSubmit(value: InferFormValue<typeof config.fields>) {\n console.log(value.email.toUpperCase()); // \u2713 Safe - always defined\n console.log(value.name?.toUpperCase()); // \u2713 Must use optional chaining\n console.log(value.age + 1); // \u2717 Error - might be undefined\n}\n```\n\n## Using InferFormValue\n\nExtract the inferred type for use in your component:\n\n```typescript\nimport { signal } from '@angular/core';\nimport { InferFormValue } from '@ng-forge/dynamic-forms';\n\n@Component({...})\nexport class UserFormComponent {\n config = {\n fields: [\n { key: 'username', type: 'input', value: '', required: true },\n { key: 'age', type: 'input', value: 0 },\n ]\n } as const satisfies FormConfig;\n\n // Extract type for signal\n formValue = signal<InferFormValue<typeof this.config.fields>>({\n username: '',\n });\n\n onSubmit() {\n const value = this.formValue();\n console.log(value.username); // \u2713 Type: string (required)\n console.log(value.age); // \u2713 Type: number | undefined (optional)\n }\n}\n```\n\n## IntelliSense Support\n\nTypeScript provides autocomplete for form values throughout your code:\n\n```typescript\nconst config = {\n fields: [\n { key: 'firstName', type: 'input', value: '' },\n { key: 'lastName', type: 'input', value: '' },\n { key: 'email', type: 'input', value: '', required: true },\n ],\n} as const satisfies FormConfig;\n\ntype FormValue = InferFormValue<typeof config.fields>;\n\nfunction processForm(value: FormValue) {\n // IntelliSense suggests: firstName, lastName, email\n value.firstName; // \u2713 Autocomplete works\n value.lastName; // \u2713 Autocomplete works\n value.email; // \u2713 Autocomplete works\n value.invalid; // \u2717 TypeScript error - property doesn't exist\n}\n```\n\n## Best Practices\n\n### Always Use `as const satisfies FormConfig`\n\nThis enables type inference and validates your configuration:\n\n```typescript\n// \u2713 Correct - enables inference + type checking\nconst config = { fields: [...] } as const satisfies FormConfig;\n\n// \u2717 Wrong - no inference\nconst config = { fields: [...] };\n\n// \u2717 Wrong - too wide, loses precision\nconst config: FormConfig = { fields: [...] };\n```\n\n### Define Configs as Constants\n\nExport form configs as constants for reuse and consistent typing:\n\n```typescript\n// form-configs.ts\nexport const USER_FORM = {\n fields: [\n { key: 'name', type: 'input', value: '', required: true },\n { key: 'email', type: 'input', value: '', required: true },\n ],\n} as const satisfies FormConfig;\n\n// user-form.component.ts\nimport { USER_FORM } from './form-configs';\n\n@Component({...})\nexport class UserFormComponent {\n config = USER_FORM;\n}\n```\n\n### Always Include `value` Property\n\nType inference requires the `value` property to determine field types:\n\n```typescript\n// \u2713 Good - type inferred from value\nconst config = {\n fields: [\n { key: 'age', type: 'input', value: 0 }, // number\n { key: 'name', type: 'input', value: '' }, // string\n { key: 'active', type: 'checkbox', value: false }, // boolean\n ],\n} as const satisfies FormConfig;\n\n// \u2717 Bad - without value, type inference may not work correctly\nconst config = {\n fields: [\n { key: 'age', type: 'input' }, // Type unclear\n ],\n} as const satisfies FormConfig;\n```\n\n### Extract Types When Needed\n\nCreate type aliases for complex form values:\n\n```typescript\nimport { InferFormValue } from '@ng-forge/dynamic-forms';\n\nconst REGISTRATION_FORM = {\n fields: [\n { key: 'username', type: 'input', value: '', required: true },\n { key: 'email', type: 'input', value: '', required: true },\n { key: 'password', type: 'input', value: '', required: true },\n ],\n} as const satisfies FormConfig;\n\n// Export type for reuse\nexport type RegistrationFormValue = InferFormValue<(typeof REGISTRATION_FORM)['fields']>;\n\n// Use in functions\nfunction registerUser(data: RegistrationFormValue) {\n console.log(data.username, data.email, data.password);\n}\n```\n\n## Troubleshooting\n\n### Type inference not working\n\nType inference requires `as const` - without it, TypeScript treats your config as mutable:\n\n```typescript\n// \u2717 No inference - types are too wide\nconst config = {\n fields: [{ key: 'name', type: 'input', value: '' }],\n};\n// Type: { fields: { key: string; type: string; value: string }[] }\n\n// \u2713 With as const - precise inference\nconst config = {\n fields: [{ key: 'name', type: 'input', value: '' }],\n} as const satisfies FormConfig;\n// Type inferred: { name?: string }\n```\n\n### `satisfies` vs type annotation\n\nDon't use type annotation (`const config: FormConfig = ...`) as it widens types:\n\n```typescript\n// \u2717 Wrong - type is widened, inference fails\nconst config: FormConfig = {\n fields: [{ key: 'name', type: 'input', value: '' }],\n} as const;\n\n// \u2713 Correct - satisfies validates without widening\nconst config = {\n fields: [{ key: 'name', type: 'input', value: '' }],\n} as const satisfies FormConfig;\n```\n\n### Dynamic form configs\n\nType inference only works for **static**, **compile-time constant** configurations:\n\n```typescript\n// \u2717 Dynamic - no inference possible\nconst fields = getFieldsFromAPI(); // Returns field array at runtime\nconst config = { fields } as const satisfies FormConfig;\n// Can't infer - TypeScript doesn't know what getFieldsFromAPI() returns\n\n// For dynamic forms, manually type your form values:\ninterface MyFormValue {\n name: string;\n email: string;\n}\n\nconst formValue = signal<MyFormValue>({ name: '', email: '' });\n```\n\nIf your form configuration is built dynamically (from API data, conditional logic, or runtime calculations), you'll need to manually define the form value type.\n\n## Container Fields\n\nUnderstanding how container fields affect type inference and form value structure.\n\n### Container Fields Overview\n\nContainer fields organize form layout and structure without directly contributing values. There are three types:\n\n- **Group Fields** - Nest children under a single key\n- **Row Fields** - Organize fields horizontally, flatten children to parent level\n- **Page Fields** - Multi-step forms, flatten children to parent level\n\nEach container type has different behavior for type inference and form values.\n\n### Group Fields\n\nGroups nest children under a single key, creating nested objects in the form value:\n\n```typescript\nconst config = {\n fields: [\n {\n type: 'group',\n key: 'address',\n label: 'Address Information',\n fields: [\n { key: 'street', type: 'input', value: '' },\n { key: 'city', type: 'input', value: '' },\n { key: 'zip', type: 'input', value: '' },\n ],\n },\n ],\n} as const satisfies FormConfig;\n\n// Inferred type:\n// {\n// address: {\n// street?: string;\n// city?: string;\n// zip?: string;\n// }\n// }\n```\n\n**Use groups when:**\n\n- You want nested form values (e.g., `address.street`)\n- Grouping related fields logically\n- Creating reusable field sections\n\n**Example - Nested address:**\n\n```typescript\nconst config = {\n fields: [\n { key: 'name', type: 'input', value: '', required: true },\n {\n type: 'group',\n key: 'address',\n fields: [\n { key: 'street', type: 'input', value: '', required: true },\n { key: 'city', type: 'input', value: '', required: true },\n { key: 'state', type: 'select', value: '', required: true, options: [] },\n { key: 'zip', type: 'input', value: '' },\n ],\n },\n ],\n} as const satisfies FormConfig;\n\n// Form value structure:\n// {\n// name: string;\n// address: {\n// street: string;\n// city: string;\n// state: string;\n// zip?: string;\n// }\n// }\n\nfunction onSubmit(value: InferFormValue<typeof config.fields>) {\n console.log(value.name); // string\n console.log(value.address.street); // string\n console.log(value.address.city); // string\n console.log(value.address.zip); // string | undefined\n}\n```\n\n### Row Fields\n\nRows organize fields horizontally for layout purposes, but flatten children to the parent level in form values:\n\n```typescript\nconst config = {\n fields: [\n {\n type: 'row',\n fields: [\n { key: 'firstName', type: 'input', value: '' },\n { key: 'lastName', type: 'input', value: '' },\n ],\n },\n ],\n} as const satisfies FormConfig;\n\n// Inferred type (flattened):\n// {\n// firstName?: string;\n// lastName?: string;\n// }\n```\n\n**Use rows when:**\n\n- You want horizontal layout (grid columns)\n- Fields should be at the parent level (not nested)\n- Creating responsive multi-column forms\n\n**Row vs Group:**\n\n```typescript\n// Row - fields are flattened\n{\n type: 'row',\n fields: [\n { key: 'firstName', type: 'input', value: '' },\n { key: 'lastName', type: 'input', value: '' },\n ],\n}\n// Result: { firstName?: string, lastName?: string }\n\n// Group - fields are nested\n{\n type: 'group',\n key: 'name',\n fields: [\n { key: 'firstName', type: 'input', value: '' },\n { key: 'lastName', type: 'input', value: '' },\n ],\n}\n// Result: { name: { firstName?: string, lastName?: string } }\n```\n\n### Page Fields\n\nPages organize multi-step forms, flattening all children to the root level:\n\n```typescript\nconst config = {\n fields: [\n {\n key: 'page1',\n type: 'page',\n fields: [\n { key: 'page1Title', type: 'text', label: 'Personal Information', props: { elementType: 'h3' } },\n { key: 'email', type: 'input', value: '' },\n { key: 'password', type: 'input', value: '' },\n ],\n },\n {\n key: 'page2',\n type: 'page',\n fields: [\n { key: 'page2Title', type: 'text', label: 'Profile Details', props: { elementType: 'h3' } },\n { key: 'firstName', type: 'input', value: '' },\n { key: 'lastName', type: 'input', value: '' },\n ],\n },\n ],\n} as const satisfies FormConfig;\n\n// Inferred type (all pages flattened):\n// {\n// email?: string;\n// password?: string;\n// firstName?: string;\n// lastName?: string;\n// }\n```\n\n**Use pages when:**\n\n- Creating multi-step forms (wizard-style)\n- Fields from all steps should be at root level\n- You want step-by-step validation\n\n### Nesting Rules\n\nContainer fields enforce nesting constraints at compile-time to prevent invalid structures.\n\n**Visual Reference:**\n\n```\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 NESTING RULES \u2502\n\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u2502\n\u2502 PAGE \u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u25BA ROW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u25BA GROUP \u2500\u2500\u252C\u2500\u2500\u25BA LEAF FIELD \u2502\n\u2502 \u2502 \u2502 \u2502 \u2502\n\u2502 \u251C\u2500\u2500\u25BA GROUP \u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u25BA ROW \u2500\u2500\u2500\u2500\u2534\u2500\u2500\u25BA LEAF FIELD \u2502\n\u2502 \u2502 \u2502 \u2502\n\u2502 \u2514\u2500\u2500\u25BA LEAF FIELD \u2514\u2500\u2500\u25BA LEAF FIELD \u2502\n\u2502 \u2502\n\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u2717 PAGE cannot contain PAGE \u2502\n\u2502 \u2717 ROW cannot contain ROW or PAGE \u2502\n\u2502 \u2717 GROUP cannot contain GROUP or PAGE \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n**Valid Nesting:**\n\n- **Pages** can contain: rows, groups, leaf fields (not other pages)\n- **Rows** can contain: groups, leaf fields (not pages or rows)\n- **Groups** can contain: rows, leaf fields (not pages or other groups)\n\n**Invalid Nesting:**\n\nTypeScript will prevent these invalid structures:\n\n```typescript\n// \u2717 Invalid: page inside page\nconst config1 = {\n fields: [\n {\n type: 'page',\n fields: [\n { type: 'page', fields: [] }, // TypeScript error!\n ],\n },\n ],\n} as const satisfies FormConfig;\n\n// \u2717 Invalid: row inside row\nconst config2 = {\n fields: [\n {\n type: 'row',\n fields: [\n { type: 'row', fields: [] }, // TypeScript error!\n ],\n },\n ],\n} as const satisfies FormConfig;\n```\n\n### Value-Bearing vs Display Fields\n\nFields are categorized by whether they contribute to form values:\n\n**Value-Bearing Fields:**\n\nFields with a `value` property contribute to the form output:\n\n- `input`, `select`, `checkbox`, `textarea`, `datepicker`, `slider`, `toggle`, etc.\n\n**Display-Only Fields:**\n\nFields without values are excluded from form values:\n\n- `text` - displays content\n- `page`, `row`, `group` - container fields (children may have values)\n- Buttons - `submit`, `reset`, navigation buttons\n- Custom display components\n\n```typescript\nconst config = {\n fields: [\n { type: 'text', label: 'Enter your details:' }, // \u2717 Excluded\n { key: 'name', type: 'input', value: '' }, // \u2713 Included\n { type: 'submit', label: 'Save' }, // \u2717 Excluded\n {\n type: 'row', // \u2717 Excluded (container)\n fields: [\n { key: 'city', type: 'input', value: '' }, // \u2713 Included\n ],\n },\n ],\n} as const satisfies FormConfig;\n\n// Only value-bearing fields:\n// {\n// name?: string;\n// city?: string;\n// }\n```\n\n### Array Fields\n\nMulti-select fields and multi-checkbox fields return arrays:\n\n```typescript\nconst config = {\n fields: [\n {\n key: 'skills',\n type: 'select',\n value: [],\n props: { multiple: true },\n options: [\n { value: 'js', label: 'JavaScript' },\n { value: 'ts', label: 'TypeScript' },\n { value: 'go', label: 'Go' },\n ],\n },\n {\n key: 'interests',\n type: 'multi-checkbox',\n value: [],\n options: [\n { value: 'sports', label: 'Sports' },\n { value: 'music', label: 'Music' },\n { value: 'reading', label: 'Reading' },\n ],\n },\n ],\n} as const satisfies FormConfig;\n\n// Type: { skills?: string[]; interests?: string[] }\n```\n\n## Advanced Patterns\n\n### Runtime Validation\n\nForm values are typed at compile-time via `InferFormValue`, but runtime validation may be needed:\n\n#### Type Assertion (Trust Form Structure)\n\nIf you trust the form structure, cast the value:\n\n```typescript\nimport { InferFormValue } from '@ng-forge/dynamic-forms';\n\nconst USER_FORM = {\n fields: [\n { key: 'username', type: 'input', value: '', required: true },\n { key: 'email', type: 'input', value: '', required: true },\n { key: 'age', type: 'input', value: 0 },\n ],\n} as const satisfies FormConfig;\n\ntype UserFormValue = InferFormValue<(typeof USER_FORM)['fields']>;\n\nfunction onSubmit(value: unknown) {\n // Cast to inferred type\n const data = value as UserFormValue;\n\n console.log(data.username); // string\n console.log(data.email); // string\n console.log(data.age); // number | undefined\n}\n```\n\n#### Runtime Validation with Zod\n\nFor runtime guarantees, use a validation library like Zod:\n\n```typescript\nimport { z } from 'zod';\nimport { InferFormValue } from '@ng-forge/dynamic-forms';\n\nconst USER_FORM = {\n fields: [\n { key: 'username', type: 'input', value: '', required: true },\n { key: 'email', type: 'input', value: '', required: true },\n { key: 'age', type: 'input', value: 0 },\n ],\n} as const satisfies FormConfig;\n\n// Define Zod schema matching form structure\nconst userSchema = z.object({\n username: z.string().min(1),\n email: z.string().email(),\n age: z.number().optional(),\n});\n\nfunction onSubmit(value: unknown) {\n // Runtime validation\n const result = userSchema.safeParse(value);\n\n if (!result.success) {\n console.error('Validation failed:', result.error);\n return;\n }\n\n // Type-safe access\n const data = result.data;\n console.log(data.username); // string\n console.log(data.email); // string\n console.log(data.age); // number | undefined\n}\n```\n\n#### Combining Compile-Time and Runtime\n\nUse both for maximum safety:\n\n```typescript\nimport { z } from 'zod';\nimport { InferFormValue } from '@ng-forge/dynamic-forms';\n\nconst REGISTRATION_FORM = {\n fields: [\n { key: 'username', type: 'input', value: '', required: true },\n { key: 'email', type: 'input', value: '', required: true },\n { key: 'password', type: 'input', value: '', required: true },\n { key: 'confirmPassword', type: 'input', value: '', required: true },\n ],\n} as const satisfies FormConfig;\n\n// Compile-time type\ntype RegistrationValue = InferFormValue<typeof REGISTRATION_FORM['fields']>;\n\n// Runtime schema (with additional validation)\nconst registrationSchema = z.object({\n username: z.string().min(3).max(20),\n email: z.string().email(),\n password: z.string().min(8),\n confirmPassword: z.string(),\n}).refine((data) => data.password === data.confirmPassword, {\n message: \"Passwords don't match\",\n path: ['confirmPassword'],\n});\n\n@Component({...})\nexport class RegistrationComponent {\n config = REGISTRATION_FORM;\n\n // Compile-time type for signal\n formValue = signal<RegistrationValue>({\n username: '',\n email: '',\n password: '',\n confirmPassword: '',\n });\n\n onSubmit() {\n const value = this.formValue();\n\n // Runtime validation with cross-field check\n const result = registrationSchema.safeParse(value);\n\n if (!result.success) {\n console.error('Validation failed:', result.error);\n return;\n }\n\n // Safe to use\n this.register(result.data);\n }\n}\n```\n\n### Type-Safe Validation\n\nng-forge integrates validation with the type system. Validators affect the inferred type:\n\n#### Shorthand Validators\n\nShorthand validators are simple and affect type inference:\n\n```typescript\nconst config = {\n fields: [\n {\n key: 'email',\n type: 'input',\n value: '',\n required: true, // Removes undefined from type\n email: true, // Email validation\n minLength: 5, // Min length validation\n },\n ],\n} as const satisfies FormConfig;\n\n// Type inferred: { email: string } (required, so no undefined)\n```\n\n#### Validator Array\n\nFor complex validation, use the `validators` array:\n\n```typescript\nconst config = {\n fields: [\n {\n key: 'discount',\n type: 'input',\n value: 0,\n validators: [\n {\n type: 'min',\n value: 0,\n },\n {\n type: 'max',\n value: 100,\n when: {\n type: 'fieldValue',\n fieldPath: 'discountType',\n operator: 'equals',\n value: 'percentage',\n },\n },\n ],\n },\n ],\n} as const satisfies FormConfig;\n```\n\n#### Conditional Required\n\nUse the `logic` array for conditional required fields:\n\n```typescript\nconst config = {\n fields: [\n { key: 'accountType', type: 'select', value: '', options: [] },\n {\n key: 'taxId',\n type: 'input',\n value: '',\n logic: [\n {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n },\n ],\n },\n ],\n} as const satisfies FormConfig;\n\n// Type: { accountType?: string; taxId?: string }\n// Note: Conditional required doesn't affect type inference\n```\n\n## Related\n\n- **[Validation](../../validation/basics/)** - How validators affect inferred types\n- **[Field Types](../../schema-fields/field-types/)** - Understanding available field types\n- **[Form Layout](../../prebuilt/form-groups/)** - Visual guide to container fields"
33
- },
34
- {
35
- id: "advanced-value-exclusion",
36
- title: "Advanced Value Exclusion",
37
- category: "advanced",
38
- content: "Value exclusion controls which field values are included in the form submission output based on the field's reactive state (hidden, disabled, readonly).\n\n## Overview\n\nBy default, field values are **excluded** from the `(submitted)` output when the field is hidden, disabled, or readonly. This prevents stale or irrelevant data from being sent to the server.\n\n**Key point:** Value exclusion only affects the submission output. Internal form controls retain their values so they can be restored when a field becomes visible, enabled, or editable again. Two-way binding via `[value]` / `[(value)]` is **not affected**.\n\n## Default Behavior\n\nWhen no configuration is specified, the defaults are:\n\n| Rule | Default | Effect |\n| ------------------------ | ------- | --------------------------------------------------- |\n| `excludeValueIfHidden` | `true` | Hidden field values are excluded from submission |\n| `excludeValueIfDisabled` | `true` | Disabled field values are excluded from submission |\n| `excludeValueIfReadonly` | `true` | Read-only field values are excluded from submission |\n\n> **Breaking change from pre-1.0:** Previously, all field values were always included. To restore the old behavior, see [Migration from Previous Versions](#migration-from-previous-versions).\n\n## 3-Tier Configuration\n\nValue exclusion supports a 3-tier configuration hierarchy. The most specific level wins for each property:\n\n| Priority | Level | Where to set |\n| -------- | ------ | ----------------------------------------------- |\n| 1 (wins) | Field | `excludeValueIf*` on individual `FieldDef` |\n| 2 | Form | `excludeValueIf*` on `FormOptions` |\n| 3 | Global | `withValueExclusionDefaults()` feature function |\n\nIf a property is `undefined` at a given level, the next level down is checked.\n\n## Global Configuration\n\nUse `withValueExclusionDefaults()` in your provider setup to configure global defaults:\n\n```typescript\nimport { provideDynamicForm, withValueExclusionDefaults } from '@ng-forge/dynamic-forms';\n\n// All exclusions enabled (same as default)\nprovideDynamicForm(...withMaterialFields(), withValueExclusionDefaults());\n\n// Disable readonly exclusion globally\nprovideDynamicForm(...withMaterialFields(), withValueExclusionDefaults({ excludeValueIfReadonly: false }));\n\n// Disable all exclusions globally (pre-1.0 behavior)\nprovideDynamicForm(\n ...withMaterialFields(),\n withValueExclusionDefaults({\n excludeValueIfHidden: false,\n excludeValueIfDisabled: false,\n excludeValueIfReadonly: false,\n }),\n);\n```\n\n## Form-Level Configuration\n\nOverride the global setting for a specific form using `FormOptions`:\n\n```typescript\nconst config: FormConfig = {\n fields: [...],\n options: {\n // Include hidden field values for this form only\n excludeValueIfHidden: false,\n // Use global default for disabled and readonly\n }\n};\n```\n\n## Field-Level Configuration\n\nOverride both global and form settings for a specific field:\n\n```typescript\nconst config: FormConfig = {\n fields: [\n {\n type: 'input',\n key: 'internalId',\n label: 'Internal ID',\n readonly: true,\n // Always include this field's value, even when readonly\n excludeValueIfReadonly: false,\n },\n {\n type: 'input',\n key: 'notes',\n label: 'Notes',\n // This field uses the form/global defaults\n },\n ],\n};\n```\n\n## Resolution Example\n\nGiven these settings:\n\n- **Global:** `excludeValueIfHidden: true`\n- **Form:** `excludeValueIfHidden: false`\n- **Field A:** `excludeValueIfHidden: undefined` (not set)\n- **Field B:** `excludeValueIfHidden: true`\n\nResolution:\n\n- **Field A** uses form-level (`false`) because field-level is `undefined` &rarr; value is **included** when hidden\n- **Field B** uses field-level (`true`) &rarr; value is **excluded** when hidden\n\n## HiddenField Type\n\nThe `HiddenField` type (`type: 'hidden'`) is **not affected** by value exclusion. These fields:\n\n- Store values without rendering any UI\n- Have a `hidden()` reactive state of `false` (they are part of the form, just not rendered)\n- Are always included in submission output\n\nValue exclusion applies to the `hidden` **property** on regular fields (e.g., `{ type: 'input', hidden: true }` or fields hidden via conditional logic), not to the `HiddenField` type.\n\n## What's Excluded vs Retained\n\n| Aspect | Affected by exclusion? | Details |\n| -------------------------- | ---------------------- | ---------------------------------------- |\n| `(submitted)` output | Yes | Excluded fields are omitted |\n| `filteredFormValue` signal | Yes | Same filtering as submission |\n| `formValue` signal | No | Always contains all values |\n| `[(value)]` binding | No | Two-way binding retains all values |\n| Internal form controls | No | Fields keep their values for restoration |\n\n## Migration from Previous Versions\n\nIf you relied on all field values being present in `(submitted)` output, disable exclusion globally:\n\n```typescript\nprovideDynamicForm(\n ...withMaterialFields(),\n withValueExclusionDefaults({\n excludeValueIfHidden: false,\n excludeValueIfDisabled: false,\n excludeValueIfReadonly: false,\n }),\n);\n```\n\nOr disable per-form via `FormOptions`:\n\n```typescript\nconst config: FormConfig = {\n fields: [...],\n options: {\n excludeValueIfHidden: false,\n excludeValueIfDisabled: false,\n excludeValueIfReadonly: false,\n }\n};\n```"
39
- },
40
- {
41
- id: "ai-integration",
42
- title: "AI Integration",
43
- category: "ai-integration",
44
- content: '# AI Integration\n\nThe `@ng-forge/dynamic-form-mcp` package provides a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that enables AI assistants to generate, validate, and work with ng-forge dynamic form configurations.\n\n## Available Tools\n\nThe MCP server provides 4 focused tools:\n\n| Tool | Description | Read-only |\n| ------------------ | ----------------------------------------------------------- | --------- |\n| `ngforge_lookup` | Get documentation about field types, concepts, and patterns | \u2705 |\n| `ngforge_examples` | Get working code examples for common form patterns | \u2705 |\n| `ngforge_validate` | Validate FormConfig and get detailed error feedback | \u2705 |\n| `ngforge_scaffold` | Generate valid FormConfig skeletons | \u2705 |\n\n## Get Started\n\n### Cursor\n\nAdd to your Cursor MCP settings:\n\n```json\n{\n "ng-forge": {\n "command": "npx",\n "args": ["-y", "@ng-forge/dynamic-form-mcp"]\n }\n}\n```\n\n### VS Code with Copilot\n\nCreate `.vscode/mcp.json` in your project:\n\n```json\n{\n "servers": {\n "ng-forge": {\n "command": "npx",\n "args": ["-y", "@ng-forge/dynamic-form-mcp"]\n }\n }\n}\n```\n\n### Claude Desktop\n\nAdd to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):\n\n```json\n{\n "mcpServers": {\n "ng-forge": {\n "command": "npx",\n "args": ["-y", "@ng-forge/dynamic-form-mcp"]\n }\n }\n}\n```\n\n### JetBrains IDEs\n\nGo to **Settings > Tools > AI Assistant > Model Context Protocol (MCP)** and add:\n\n| Field | Value |\n| --------- | ----------------------------- |\n| Name | ng-forge |\n| Command | npx |\n| Arguments | -y @ng-forge/dynamic-form-mcp |\n\n------------ | --------------------------------------------------------- | ---------- | ----------------------- |\n| `topic` | string | (required) | Topic to look up |\n| `depth` | `"brief"` \\| `"full"` \\| `"schema"` | `"full"` | Level of detail |\n| `uiIntegration` | `"material"` \\| `"bootstrap"` \\| `"primeng"` \\| `"ionic"` | - | Filter UI-specific info |\n\n**Available Topics:**\n\n| Category | Topics |\n| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Field Types | `input`, `select`, `radio`, `checkbox`, `textarea`, `datepicker`, `slider`, `toggle`, `hidden`, `text`, `button`, `submit`, `next`, `previous`, `addArrayItem`, `prependArrayItem`, `insertArrayItem`, `removeArrayItem`, `popArrayItem`, `shiftArrayItem` |\n| Containers | `group`, `row`, `array`, `page` |\n| Concepts | `validation`, `conditional`, `derivation`, `options-format`, `expression-variables`, `async-validators`, `validation-messages` |\n| Patterns | `golden-path`, `multi-page-gotchas`, `pitfalls`, `workflow` |\n\n**Examples:**\n\n```\nngforge_lookup topic="hidden" depth="brief"\nngforge_lookup topic="conditional" depth="full"\nngforge_lookup topic="input" depth="schema" uiIntegration="material"\n```\n\n---\n\n### ngforge_examples\n\nGet working code examples for common patterns.\n\n| Parameter | Type | Default | Description |\n| --------- | ----------------------------------------------------- | ---------- | --------------- |\n| `pattern` | string | (required) | Pattern name |\n| `depth` | `"minimal"` \\| `"brief"` \\| `"full"` \\| `"explained"` | `"full"` | Level of detail |\n\n**Available Patterns:**\n\n| Pattern | Description | Lines |\n| --------------------- | ---------------------------------------- | ----- |\n| `minimal-multipage` | Simplest 2-page wizard form | ~50 |\n| `minimal-array` | Array with add/remove buttons | ~30 |\n| `minimal-conditional` | Show/hide field based on condition | ~25 |\n| `minimal-validation` | Password confirmation validation | ~20 |\n| `minimal-hidden` | Hidden fields in multi-page form | ~15 |\n| `derivation` | Value derivation (computed fields) | - |\n| `conditional` | Conditional visibility patterns | - |\n| `multi-page` | Multi-step wizard forms | - |\n| `validation` | Form validation patterns | - |\n| `complete` | Full form with all major features | - |\n| `mega` | Kitchen sink demonstrating every feature | - |\n\n**Examples:**\n\n```\nngforge_examples pattern="minimal-multipage" depth="minimal"\nngforge_examples pattern="conditional" depth="explained"\n```\n\n---\n\n### ngforge_validate\n\nValidate FormConfig and get detailed error feedback.\n\n| Parameter | Type | Default | Description |\n| --------------- | --------------------------------------------------------- | ------------ | ------------------------------ |\n| `config` | string \\| object | (required) | File path or JSON config |\n| `uiIntegration` | `"material"` \\| `"bootstrap"` \\| `"primeng"` \\| `"ionic"` | `"material"` | UI library to validate against |\n\n**Input Detection:**\n\n| Input | Treated As |\n| ------------------------ | ------------------ |\n| Ends with `.ts` or `.js` | File path |\n| Starts with `{` or `[` | JSON string |\n| Object | Validated directly |\n\n**Example Errors:**\n\n- "Hidden field missing REQUIRED value property"\n- "options MUST be at FIELD level, NOT inside props"\n- "row containers do NOT support logic blocks"\n\n**Examples:**\n\n```\nngforge_validate config="/path/to/form.config.ts"\nngforge_validate config=\'{"fields":[...]}\' uiIntegration="bootstrap"\n```\n\n---\n\n### ngforge_scaffold\n\nGenerate valid FormConfig skeletons.\n\n| Parameter | Type | Default | Description |\n| --------------- | -------- | ------------ | ------------------------------------- |\n| `pages` | number | `0` | Number of pages (0 = single-page) |\n| `arrays` | string[] | `[]` | Array field names |\n| `groups` | string[] | `[]` | Group field names |\n| `hidden` | string[] | `[]` | Hidden fields as `"name:value"` pairs |\n| `fields` | string[] | `[]` | Fields as `"name:type"` pairs |\n| `uiIntegration` | enum | `"material"` | UI library |\n\n**Supported Types for `fields`:**\n\n`input`, `select`, `radio`, `checkbox`, `textarea`, `datepicker`, `slider`, `toggle`\n\n**Examples:**\n\n```\nngforge_scaffold pages=0 fields=["name:input","email:input"]\nngforge_scaffold pages=3 arrays=["contacts"] groups=["address"]\nngforge_scaffold hidden=["userId:abc123","source:web"]\n```\n\n---\n\n## MCP Resources\n\nIn addition to tools, the server exposes resources that AI can read:\n\n| Resource URI | Description |\n| -------------------------- | ---------------------------------------------- |\n| `ng-forge://instructions` | Best practices guide for generating FormConfig |\n| `ng-forge://examples` | Curated FormConfig examples |\n| `ng-forge://examples/{id}` | Specific example by ID |\n| `ng-forge://field-types` | Field type reference |\n| `ng-forge://validators` | Validator reference |\n| `ng-forge://ui-adapters` | UI library configurations |\n| `ng-forge://docs` | Full documentation index |\n\n---\n\n## Feedback\n\nFound an issue or have a suggestion? [Open an issue on GitHub](https://github.com/ng-forge/ng-forge/issues).'
45
- },
46
- {
47
- id: "dynamic-behavior-conditional-logic-overview",
48
- title: "Dynamic Behavior Conditional Logic Overview",
49
- category: "dynamic-behavior",
50
- content: "Control field behavior dynamically based on form state. Dynamic forms provides a declarative API for conditional visibility, required state, and readonly state that maps directly to Angular's signal forms.\n\n## Signal Forms Integration\n\nThe library integrates with Angular's signal forms logic functions:\n\n```typescript\nimport { hidden, readonly, required } from '@angular/forms/signals';\n```\n\nAll conditional logic configuration is applied using these functions, providing:\n\n- **Reactive updates** when form state changes\n- **Type-safe** conditional expressions\n- **Automatic re-evaluation** on dependencies\n- **Integration** with form validation state\n\n## Static Properties\n\nUse simple boolean properties for fields with fixed states.\n\n### hidden\n\nHide a field from view (field still participates in form state):\n\n```typescript\n{\n key: 'internalId',\n type: 'input',\n value: '',\n label: 'Internal ID',\n hidden: true,\n}\n```\n\nThe field is hidden from the UI but still included in the form value.\n\n### disabled\n\nDisable user interaction:\n\n```typescript\n{\n key: 'systemField',\n type: 'input',\n value: 'auto-generated',\n label: 'System Field',\n disabled: true,\n}\n```\n\n**Note:** The `disabled` property is handled at the component level and does not use signal forms logic functions. It's a static UI property that prevents user interaction.\n\n### readonly\n\nMake a field read-only (displays value but prevents modification):\n\n```typescript\n{\n key: 'createdAt',\n type: 'input',\n value: '2024-01-15',\n label: 'Created Date',\n readonly: true,\n}\n```\n\n## Dynamic Conditional Logic\n\nFor conditional behavior based on form state, use the `logic` array with `LogicConfig` objects.\n\n```typescript\ninterface LogicConfig {\n /** Logic type */\n type: 'hidden' | 'readonly' | 'disabled' | 'required';\n\n /** Boolean expression, static value, or form state condition */\n condition: ConditionalExpression | boolean | FormStateCondition;\n}\n```\n\n`FormStateCondition` values (`'formInvalid'`, `'formSubmitting'`, `'pageInvalid'`) are primarily used for button disabled logic.\n\n### Conditional Visibility (hidden)\n\nShow or hide fields based on other field values.\n\n#### Show Email When Contact Method is Email\n\n```typescript\n{\n key: 'contactMethod',\n type: 'select',\n value: '',\n options: [\n { value: 'email', label: 'Email' },\n { value: 'phone', label: 'Phone' },\n ],\n}\n{\n key: 'email',\n type: 'input',\n value: '',\n label: 'Email Address',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'contactMethod',\n operator: 'notEquals',\n value: 'email',\n },\n }],\n}\n```\n\nWhen `contactMethod !== 'email'`, the email field is hidden.\n\n### Conditional Required\n\nMake fields required based on conditions.\n\n#### Tax ID Required for Business Accounts\n\n```typescript\n{\n key: 'accountType',\n type: 'radio',\n value: 'personal',\n options: [\n { value: 'personal', label: 'Personal' },\n { value: 'business', label: 'Business' },\n ],\n}\n{\n key: 'taxId',\n type: 'input',\n value: '',\n label: 'Tax ID',\n logic: [{\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n }],\n}\n```\n\n### Conditional Readonly\n\nMake fields read-only based on conditions.\n\n#### Lock Field After Submission\n\n```typescript\n{\n key: 'status',\n type: 'select',\n value: 'draft',\n options: [\n { value: 'draft', label: 'Draft' },\n { value: 'submitted', label: 'Submitted' },\n ],\n}\n{\n key: 'documentNumber',\n type: 'input',\n value: '',\n label: 'Document Number',\n logic: [{\n type: 'readonly',\n condition: {\n type: 'fieldValue',\n fieldPath: 'status',\n operator: 'equals',\n value: 'submitted',\n },\n }],\n}\n```\n\nOnce status is \"submitted\", the document number becomes read-only.\n\n## Basic Conditional Expression\n\nThe most common conditional expression checks a specific field's value:\n\n```typescript\n{\n type: 'fieldValue',\n fieldPath: 'fieldKey',\n operator: 'equals',\n value: 'expectedValue',\n}\n```\n\n**Components:**\n\n- `type: 'fieldValue'` - Check a specific field\n- `fieldPath` - The field key to check\n- `operator` - Comparison operator (see [All Operators](#all-operators))\n- `value` - Value to compare against\n\n## Quick Example\n\nShow a field only when another field has a specific value:\n\n```typescript\n{\n key: 'email',\n type: 'input',\n value: '',\n label: 'Email Address',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'contactMethod',\n operator: 'notEquals',\n value: 'email',\n },\n },\n {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'contactMethod',\n operator: 'equals',\n value: 'email',\n },\n },\n ],\n}\n```\n\nThis field is hidden unless `contactMethod === 'email'`, and required when visible. See the [Examples](/examples) page for complete form implementations.\n\n## When Logic Runs\n\nConditional logic is evaluated:\n\n- **On form value change** - Any time a dependent field changes\n- **On initialization** - When the form is created\n- **Reactively** - Uses Angular's signal forms for automatic updates\n\n### Evaluation Flow\n\n```\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 LOGIC EVALUATION FLOW \u2502\n\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u2502\n\u2502 Form Value Changes \u2502\n\u2502 \u2502 \u2502\n\u2502 \u25BC \u2502\n\u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502 \u2502 Signal Updates \u2502\u25C4\u2500\u2500\u2500\u2500 Angular's reactive system \u2502\n\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n\u2502 \u2502 \u2502\n\u2502 \u25BC \u2502\n\u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502 \u2502 Evaluate logic \u2502 \u2502\n\u2502 \u2502 conditions \u2502 \u2502\n\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n\u2502 \u2502 \u2502\n\u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502 \u25BC \u25BC \u2502\n\u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502 \u2502 true \u2502 \u2502false \u2502 \u2502\n\u2502 \u2514\u2500\u2500\u252C\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u252C\u2500\u2500\u2500\u2518 \u2502\n\u2502 \u2502 \u2502 \u2502\n\u2502 \u25BC \u25BC \u2502\n\u2502 Apply Remove \u2502\n\u2502 effect effect \u2502\n\u2502 (hide, (show, \u2502\n\u2502 require, optional, \u2502\n\u2502 readonly) editable) \u2502\n\u2502 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n## Expression Types\n\n### fieldValue\n\nCheck a specific field's value - the most common expression type.\n\n```typescript\n{\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n}\n```\n\n**Use when:** Checking a single field's value\n\n**Example:**\n\n```typescript\n{\n key: 'companyName',\n type: 'input',\n value: '',\n logic: [{\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n }],\n}\n```\n\n### formValue\n\nCompare the entire form value object against a specific value using operators.\n\n```typescript\n{\n type: 'formValue',\n operator: 'equals',\n value: { status: 'active', role: 'admin' },\n}\n```\n\n**Use when:** Checking if the entire form matches a specific state\n\n**Note:** This type is rarely useful in practice \u2014 deep equality on an entire form object is an unusual requirement. For conditions that involve multiple specific fields, use `javascript` or `custom` expressions instead (e.g. `formValue.status === 'active' && formValue.role === 'admin'`).\n\n### javascript\n\nJavaScript expressions with access to `fieldValue` (current field) and `formValue` (entire form).\n\n```typescript\n{\n type: 'javascript',\n expression: 'new Date(fieldValue) > new Date()',\n}\n```\n\n**Use when:** Custom logic on field value or complex multi-field conditions\n\n**Examples:**\n\n```typescript\n// Check current field value\n{\n key: 'eventDate',\n type: 'datepicker',\n value: null,\n logic: [{\n type: 'readonly',\n condition: {\n type: 'javascript',\n expression: 'new Date(fieldValue) < new Date()',\n },\n }],\n}\n\n// Check multiple form fields (replaces old formValue expression pattern)\n{\n key: 'stateProvince',\n type: 'select',\n value: '',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'javascript',\n expression: 'formValue.country !== \"US\" && formValue.country !== \"CA\"',\n },\n }],\n}\n```\n\n**Safe member access:** Accessing nested properties on `null` or `undefined` returns `undefined` (no errors thrown):\n\n```typescript\n{\n type: 'javascript',\n // Safe even when user, profile, or preferences is null/undefined\n expression: 'formValue.user.profile.preferences.notifications === true',\n}\n```\n\n### custom\n\nAdvanced custom expressions with access to both field and form values.\n\n```typescript\n{\n type: 'custom',\n expression: 'fieldValue > formValue.minAge && fieldValue < formValue.maxAge',\n}\n```\n\n**Safe member access:** Like `formValue` expressions, nested property access is safe:\n\n```typescript\n{\n type: 'custom',\n // Safe even when nested values are null/undefined\n expression: 'fieldValue !== formValue.user.profile.firstName',\n}\n```\n\n### Field State in Expressions\n\n`javascript` and `custom` expressions have access to two additional variables for querying field interaction state:\n\n- **`fieldState`** \u2014 the current field's own state\n- **`formFieldState`** \u2014 state of any field in the form, by key\n\n#### fieldState\n\nUse `fieldState` to react to the current field's own interaction state:\n\n```typescript\n// Lock the field as soon as the user edits it\n{\n key: 'accountNumber',\n type: 'input',\n logic: [{\n type: 'readonly',\n condition: {\n type: 'javascript',\n expression: 'fieldState.dirty',\n },\n }],\n}\n```\n\n**Available properties:**\n\n| Property | Type | Description |\n| ---------- | --------- | ----------------------------------- |\n| `touched` | `boolean` | User has focused and left the field |\n| `dirty` | `boolean` | User has changed the field value |\n| `pristine` | `boolean` | Equivalent to `!dirty` |\n| `valid` | `boolean` | Field has no validation errors |\n| `invalid` | `boolean` | Field has validation errors |\n| `pending` | `boolean` | Async validation is in progress |\n| `hidden` | `boolean` | Field is currently hidden |\n| `readonly` | `boolean` | Field is currently readonly |\n| `disabled` | `boolean` | Field is currently disabled |\n\n#### formFieldState\n\nUse `formFieldState` to react to another field's state. Access by field key:\n\n```typescript\n// Make a field readonly once a related field has been touched\n{\n key: 'billingAddress',\n type: 'input',\n logic: [{\n type: 'readonly',\n condition: {\n type: 'javascript',\n expression: 'formFieldState.shippingAddress.dirty',\n },\n }],\n}\n```\n\n`formFieldState` has the same properties as `fieldState`, keyed by field name.\n\n**Example \u2014 show a confirmation field only after the primary field is dirty:**\n\n```typescript\n{\n key: 'confirmEmail',\n type: 'input',\n label: 'Confirm Email',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'javascript',\n expression: '!formFieldState.email.dirty',\n },\n }],\n}\n```\n\n### http\n\nEvaluate a condition by sending an HTTP request and inspecting the response. The request fires automatically when declared query params change, with built-in debouncing.\n\n```typescript\n{\n type: 'http',\n http: {\n url: '/api/permissions',\n queryParams: {\n role: 'formValue.userRole',\n },\n },\n responseExpression: 'response.canEdit',\n pendingValue: false,\n}\n```\n\n**Use when:** Field visibility or state must be determined server-side (permissions, feature flags, country-specific rules).\n\n**Full example \u2014 hide an admin panel based on server permissions:**\n\n```typescript\n{\n key: 'adminPanel',\n type: 'input',\n label: 'Admin Panel Access Code',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'http',\n http: {\n url: '/api/permissions',\n queryParams: {\n role: 'formValue.userRole',\n },\n },\n responseExpression: 'response.hideAdminPanel',\n pendingValue: true, // Hide while checking\n },\n }],\n}\n```\n\n**HTTP condition properties:**\n\n| Property | Type | Required | Default | Description |\n| -------------------- | ------------------- | -------- | ------------ | ------------------------------------------------------------------------ |\n| `type` | `'http'` | Yes | \u2014 | Identifies this as an HTTP condition |\n| `http` | `HttpRequestConfig` | Yes | \u2014 | Request configuration (see below) |\n| `responseExpression` | `string` | No | `!!response` | Expression evaluated with `{ response }` in scope. Must return a boolean |\n| `pendingValue` | `boolean` | No | `false` | Value returned while the request is in-flight |\n| `cacheDurationMs` | `number` | No | `30000` | How long to cache responses (ms) |\n| `debounceMs` | `number` | No | `300` | Debounce delay before re-sending (ms) |\n\n**`HttpRequestConfig` quick reference:**\n\n| Property | Description |\n| ------------------------- | ----------------------------------------------------------------------------------- |\n| `url` | Request URL. Use `:key` placeholders for path parameters |\n| `method` | HTTP method. Defaults to `'GET'` |\n| `params` | Path parameters. `:key` placeholders in the URL replaced with evaluated expressions |\n| `queryParams` | Key/value map. Values are expressions evaluated against form context |\n| `body` | Request body for POST/PUT/PATCH |\n| `evaluateBodyExpressions` | When `true`, top-level `body` string values are evaluated as expressions |\n| `headers` | Request headers |\n\n**HTTP condition on `required` \u2014 server-driven required fields:**\n\n```typescript\n{\n key: 'taxId',\n type: 'input',\n logic: [{\n type: 'required',\n condition: {\n type: 'http',\n http: {\n url: '/api/tax-rules',\n queryParams: { country: 'formValue.country' },\n },\n responseExpression: 'response.taxIdRequired',\n pendingValue: false, // Optional while checking\n },\n }],\n}\n```\n\n### async\n\nEvaluate a condition using a custom async function registered in `customFnConfig.asyncConditions`. Functions receive the full form context and must return a `Promise<boolean>` or `Observable<boolean>`.\n\n```typescript\n{\n type: 'async',\n asyncFunctionName: 'checkPermission',\n pendingValue: false,\n}\n```\n\n**Use when:** Condition logic involves Angular service injection, complex async operations, or anything that `http` conditions cannot express directly.\n\n> **Why `inject()` works here:** `customFnConfig` functions are called within an Angular injection context, so Angular's `inject()` API is available \u2014 the same way it works in a constructor or field initializer. Import `inject` from `@angular/core` as usual.\n\n**Registration and usage:**\n\n```typescript\nimport { inject } from '@angular/core';\n\nconst formConfig = {\n customFnConfig: {\n asyncConditions: {\n checkReadonly: (context) => {\n return inject(PermissionsService).canEdit(context.formValue.resourceId as string);\n },\n },\n },\n\n fields: [\n {\n key: 'salary',\n type: 'input',\n label: 'Salary',\n logic: [\n {\n type: 'readonly',\n condition: {\n type: 'async',\n asyncFunctionName: 'checkReadonly',\n pendingValue: false, // Editable while checking\n },\n },\n ],\n },\n ],\n} as const satisfies FormConfig;\n```\n\n**Async condition properties:**\n\n| Property | Type | Required | Default | Description |\n| ------------------- | --------- | -------- | ------- | --------------------------------------------------- |\n| `type` | `'async'` | Yes | \u2014 | Identifies this as an async condition |\n| `asyncFunctionName` | `string` | Yes | \u2014 | Name registered in `customFnConfig.asyncConditions` |\n| `pendingValue` | `boolean` | No | `false` | Value returned while the function is resolving |\n| `debounceMs` | `number` | No | `300` | Debounce delay before re-evaluating (ms) |\n\n**Choosing `pendingValue`:**\n\nThe right `pendingValue` depends on the logic type and desired UX:\n\n| Logic type | `pendingValue: false` | `pendingValue: true` |\n| ---------- | ----------------------- | ----------------------- |\n| `hidden` | Visible while checking | Hidden while checking |\n| `required` | Optional while checking | Required while checking |\n| `readonly` | Editable while checking | Readonly while checking |\n| `disabled` | Enabled while checking | Disabled while checking |\n\n## All Operators\n\n### Equality Operators\n\n#### equals\n\nExact match comparison.\n\n```typescript\n{\n type: 'fieldValue',\n fieldPath: 'status',\n operator: 'equals',\n value: 'active',\n}\n```\n\n#### notEquals\n\nNot equal to comparison.\n\n```typescript\n{\n type: 'fieldValue',\n fieldPath: 'status',\n operator: 'notEquals',\n value: 'archived',\n}\n```\n\n### Comparison Operators\n\n#### greater\n\nGreater than comparison (numbers/dates).\n\n```typescript\n{\n type: 'fieldValue',\n fieldPath: 'age',\n operator: 'greater',\n value: 18,\n}\n```\n\n#### less\n\nLess than comparison.\n\n```typescript\n{\n type: 'fieldValue',\n fieldPath: 'quantity',\n operator: 'less',\n value: 100,\n}\n```\n\n#### greaterOrEqual\n\nGreater than or equal to.\n\n```typescript\n{\n type: 'fieldValue',\n fieldPath: 'age',\n operator: 'greaterOrEqual',\n value: 21,\n}\n```\n\n#### lessOrEqual\n\nLess than or equal to.\n\n```typescript\n{\n type: 'fieldValue',\n fieldPath: 'discount',\n operator: 'lessOrEqual',\n value: 100,\n}\n```\n\n### String Operators\n\n#### contains\n\nCheck if string/array contains value.\n\n```typescript\n{\n type: 'fieldValue',\n fieldPath: 'email',\n operator: 'contains',\n value: '@company.com',\n}\n```\n\n#### startsWith\n\nCheck if string starts with value.\n\n```typescript\n{\n type: 'fieldValue',\n fieldPath: 'url',\n operator: 'startsWith',\n value: 'https://',\n}\n```\n\n#### endsWith\n\nCheck if string ends with value.\n\n```typescript\n{\n type: 'fieldValue',\n fieldPath: 'email',\n operator: 'endsWith',\n value: '.gov',\n}\n```\n\n#### matches\n\nRegular expression match.\n\n```typescript\n{\n type: 'fieldValue',\n fieldPath: 'zipCode',\n operator: 'matches',\n value: '^[0-9]{5}$',\n}\n```\n\n## Combining Conditions\n\n### AND Logic\n\nAll conditions must be true.\n\n```typescript\n{\n type: 'and',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n {\n type: 'fieldValue',\n fieldPath: 'hasTeam',\n operator: 'equals',\n value: true,\n },\n {\n type: 'fieldValue',\n fieldPath: 'teamSize',\n operator: 'greater',\n value: 5,\n },\n ],\n}\n```\n\n**Use case:** Field required when all conditions are met.\n\n```typescript\n{\n key: 'enterpriseFeatures',\n type: 'checkbox',\n label: 'Enable Enterprise Features',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'and',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n {\n type: 'fieldValue',\n fieldPath: 'plan',\n operator: 'equals',\n value: 'enterprise',\n },\n ],\n },\n }],\n}\n```\n\n### OR Logic\n\nAt least one condition must be true.\n\n```typescript\n{\n type: 'or',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'role',\n operator: 'equals',\n value: 'admin',\n },\n {\n type: 'fieldValue',\n fieldPath: 'role',\n operator: 'equals',\n value: 'owner',\n },\n ],\n}\n```\n\n**Use case:** Show field for multiple roles \u2014 hide unless role is `admin` or `owner`.\n\n```typescript\n// Hide the panel when role is neither 'admin' nor 'owner'\n// (i.e. hidden when notEquals 'admin' AND notEquals 'owner')\n{\n key: 'adminPanel',\n type: 'group',\n label: 'Administration',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'and',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'role',\n operator: 'notEquals',\n value: 'admin',\n },\n {\n type: 'fieldValue',\n fieldPath: 'role',\n operator: 'notEquals',\n value: 'owner',\n },\n ],\n },\n }],\n}\n```\n\n### Nested Logic\n\nCombine AND/OR logic for complex conditions.\n\n```typescript\n{\n type: 'and',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'country',\n operator: 'equals',\n value: 'US',\n },\n {\n type: 'or',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'age',\n operator: 'greaterOrEqual',\n value: 21,\n },\n {\n type: 'fieldValue',\n fieldPath: 'hasParentalConsent',\n operator: 'equals',\n value: true,\n },\n ],\n },\n ],\n}\n```\n\nThis means: \"Country must be US AND (age >= 21 OR has parental consent)\"\n\n## Practical Examples\n\n### Show Field Based on Multiple Conditions\n\n```typescript\n{\n key: 'internationalShipping',\n type: 'checkbox',\n label: 'Enable International Shipping',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'or',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'free',\n },\n {\n type: 'fieldValue',\n fieldPath: 'verified',\n operator: 'equals',\n value: false,\n },\n ],\n },\n }],\n}\n```\n\nHidden for free accounts OR unverified accounts.\n\n### Required Field with Complex Logic\n\n```typescript\n{\n key: 'taxExemptionNumber',\n type: 'input',\n value: '',\n label: 'Tax Exemption Number',\n logic: [{\n type: 'required',\n condition: {\n type: 'and',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n {\n type: 'fieldValue',\n fieldPath: 'claimsTaxExemption',\n operator: 'equals',\n value: true,\n },\n {\n type: 'or',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'country',\n operator: 'equals',\n value: 'US',\n },\n {\n type: 'fieldValue',\n fieldPath: 'country',\n operator: 'equals',\n value: 'CA',\n },\n ],\n },\n ],\n },\n }],\n}\n```\n\n### Dynamic Read-Only Based on Status\n\n```typescript\n{\n key: 'orderItems',\n type: 'group',\n label: 'Order Items',\n logic: [{\n type: 'readonly',\n condition: {\n type: 'or',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'orderStatus',\n operator: 'equals',\n value: 'shipped',\n },\n {\n type: 'fieldValue',\n fieldPath: 'orderStatus',\n operator: 'equals',\n value: 'delivered',\n },\n {\n type: 'fieldValue',\n fieldPath: 'orderStatus',\n operator: 'equals',\n value: 'cancelled',\n },\n ],\n },\n }],\n}\n```\n\nOrder items become read-only once order is shipped, delivered, or cancelled.\n\n## Best Practices\n\n**Keep conditions readable:**\n\n```typescript\n// \u2705 Good - Easy to understand\n{\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n}\n\n// \u274C Avoid - Hard to maintain\n{\n type: 'formValue',\n expression: 'formValue.accountType === \"business\" && formValue.country !== null && formValue.hasTeam',\n}\n```\n\n## ConditionalExpression Interface\n\n`ConditionalExpression` is a discriminated union of all expression types:\n\n```typescript\n// Sync expressions\ntype ConditionalExpression =\n | { type: 'fieldValue'; fieldPath: string; operator: Operator; value: unknown }\n | { type: 'formValue'; operator: Operator; value: unknown }\n | { type: 'javascript'; expression: string }\n | { type: 'custom'; expression: string }\n | { type: 'and'; conditions: ConditionalExpression[] }\n | { type: 'or'; conditions: ConditionalExpression[] }\n // Async expressions\n | HttpCondition\n | AsyncCondition;\n\ntype Operator =\n | 'equals'\n | 'notEquals'\n | 'greater'\n | 'less'\n | 'greaterOrEqual'\n | 'lessOrEqual'\n | 'contains'\n | 'startsWith'\n | 'endsWith'\n | 'matches';\n\ninterface HttpCondition {\n type: 'http';\n http: HttpRequestConfig;\n responseExpression?: string; // Evaluated with { response }. Defaults to !!response\n pendingValue?: boolean; // Default: false\n cacheDurationMs?: number; // Default: 30000\n debounceMs?: number; // Default: 300\n}\n\ninterface AsyncCondition {\n type: 'async';\n asyncFunctionName: string;\n pendingValue?: boolean; // Default: false\n debounceMs?: number; // Default: 300\n}\n```\n\n**Expression types summary:**\n\n| Type | Sync/Async | Key properties | Purpose |\n| ------------ | ---------- | -------------------------------- | --------------------------------------------------------------------- |\n| `fieldValue` | Sync | `fieldPath`, `operator`, `value` | Compare a specific field's value |\n| `formValue` | Sync | `operator`, `value` | Compare entire form object |\n| `javascript` | Sync | `expression` | Custom JS with `fieldValue`/`formValue`/`fieldState`/`formFieldState` |\n| `custom` | Sync | `expression` | Inline expression with `fieldValue`/`formValue` (safe member access) |\n| `and`/`or` | Sync | `conditions` | Combine multiple conditions |\n| `http` | Async | `http`, `responseExpression` | Server-driven condition via HTTP request |\n| `async` | Async | `asyncFunctionName` | Custom async function registered in config |\n\n## Common Patterns\n\n### Show/Hide Field Pattern\n\n```typescript\nlogic: [\n {\n type: 'hidden',\n condition: {\n /* when to hide */\n },\n },\n];\n```\n\n### Conditional Required Pattern\n\n```typescript\nlogic: [\n {\n type: 'hidden',\n condition: {\n /* when to hide */\n },\n },\n {\n type: 'required',\n condition: {\n /* when to require */\n },\n },\n];\n```\n\n### Multiple Conditions Pattern\n\n```typescript\nlogic: [\n {\n type: 'hidden',\n condition: {\n type: 'and', // or 'or'\n conditions: [\n {\n /* condition 1 */\n },\n {\n /* condition 2 */\n },\n ],\n },\n },\n];\n```\n\n## External Data in Conditions\n\nAccess external application state (user roles, permissions, feature flags) in conditions using `externalData`:\n\n```typescript\nconst config = {\n externalData: {\n userRole: computed(() => authService.role()),\n featureFlags: computed(() => ({\n advancedMode: featureService.isAdvanced(),\n })),\n },\n fields: [\n {\n key: 'adminNotes',\n type: 'textarea',\n label: 'Admin Notes',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'javascript',\n expression: \"externalData.userRole !== 'admin'\",\n },\n },\n ],\n },\n {\n key: 'advancedSettings',\n type: 'input',\n label: 'Advanced Settings',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'javascript',\n expression: 'externalData.featureFlags.advancedMode !== true',\n },\n },\n ],\n },\n ],\n} as const satisfies FormConfig;\n```\n\n**Key points:**\n\n- Each property in `externalData` must be a Signal (`signal()` or `computed()`)\n- Changes to external signals automatically re-evaluate conditions\n- Access values via `externalData.propertyName` in JavaScript expressions\n\n## Related\n\n- **[Value Derivation](../value-derivation/basics/)** \u2014 Computed field values\n- **[Async Derivation](../value-derivation/async/)** \u2014 HTTP and async function derivations, stopOnUserOverride\n- **[Validation](../../validation/basics/)** \u2014 Conditional validation\n- **[Custom Validators](../../validation/custom-validators/)** \u2014 Async and HTTP validators\n- **[Type Safety](../../advanced/type-safety/basics/)** \u2014 TypeScript integration\n- **[Examples](/examples)** \u2014 Real-world form patterns"
51
- },
52
- {
53
- id: "dynamic-behavior-i18n",
54
- title: "Dynamic Behavior I18n",
55
- category: "dynamic-behavior",
56
- content: "Dynamic Forms supports internationalization through Angular's reactive primitives: **Observables** and **Signals**. It's framework-agnostic - use any translation library that provides these types.\n\n## How i18n Works\n\nDynamic Forms uses the `DynamicText` type for all text properties:\n\n```typescript\ntype DynamicText = string | Observable<string> | Signal<string>;\n```\n\nAny property that accepts text (`label`, `placeholder`, `validationMessages`, and UI-integration specific props like `hint`) accepts:\n\n- Static strings\n- Observables (from translation libraries like Transloco, ngx-translate, etc.)\n- Signals\n\n## Basic Example\n\nWith any translation service that returns Observables:\n\n```typescript\nimport { Component, inject } from '@angular/core';\n\n@Component({...})\nexport class MyFormComponent {\n translationService = inject(YourTranslationService);\n\n config = {\n fields: [\n {\n key: 'firstName',\n type: 'input',\n label: this.translationService.translate('form.firstName'), // Observable<string>\n value: '',\n },\n {\n key: 'email',\n type: 'input',\n label: this.translationService.translate('form.email'),\n email: true,\n validationMessages: {\n required: this.translationService.translate('validation.required'),\n email: this.translationService.translate('validation.email'),\n },\n value: '',\n },\n ],\n };\n}\n```\n\nThe form automatically updates when translations change.\n\n## Default Validation Messages\n\nDefine common validation messages once at the form level using `defaultValidationMessages`. These act as fallback messages when fields don't have their own custom `validationMessages`:\n\n```typescript\nimport { Component, inject } from '@angular/core';\n\n@Component({...})\nexport class MyFormComponent {\n translationService = inject(YourTranslationService);\n\n config = {\n // Define default messages for all fields\n defaultValidationMessages: {\n required: this.translationService.translate('validation.required'),\n email: this.translationService.translate('validation.email'),\n minLength: this.translationService.translate('validation.minLength'),\n maxLength: this.translationService.translate('validation.maxLength'),\n },\n fields: [\n {\n key: 'email',\n type: 'input',\n label: this.translationService.translate('form.email'),\n email: true,\n required: true,\n // Uses defaultValidationMessages for required and email errors\n },\n {\n key: 'password',\n type: 'input',\n label: this.translationService.translate('form.password'),\n required: true,\n minLength: 8,\n // Override default for this field only\n validationMessages: {\n required: this.translationService.translate('validation.password.required'),\n minLength: this.translationService.translate('validation.password.minLength'),\n },\n },\n {\n key: 'username',\n type: 'input',\n label: this.translationService.translate('form.username'),\n required: true,\n minLength: 3,\n // Uses default for both required and minLength\n },\n ],\n };\n}\n```\n\nThe message resolution priority is:\n\n1. **Field-level** `validationMessages` (highest priority)\n2. **Form-level** `defaultValidationMessages` (fallback)\n3. **No message** - If neither is provided, the error is not displayed and a warning is logged to the console\n\nThis approach is especially useful when you have many fields with the same validation rules - define the translations once instead of repeating them for each field.\n\n## Example with Transloco\n\nHere's a complete example using [@jsverse/transloco](https://jsverse.github.io/transloco/):\n\n```typescript name=\"app.config.ts\"\nimport { ApplicationConfig } from '@angular/core';\nimport { provideHttpClient } from '@angular/common/http';\nimport { provideTransloco } from '@jsverse/transloco';\nimport { provideDynamicForm } from '@ng-forge/dynamic-forms';\nimport { withMaterialFields } from '@ng-forge/dynamic-forms-material';\n\nexport const appConfig: ApplicationConfig = {\n providers: [\n provideHttpClient(),\n provideTransloco({\n config: {\n availableLangs: ['en', 'es'],\n defaultLang: 'en',\n reRenderOnLangChange: true,\n },\n loader: TranslocoHttpLoader,\n }),\n provideDynamicForm(...withMaterialFields()),\n ],\n};\n```\n\n```typescript name=\"my-form.component.ts\"\nimport { Component, inject } from '@angular/core';\nimport { TranslocoService } from '@jsverse/transloco';\nimport { DynamicForm } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-my-form',\n imports: [DynamicForm],\n template: ` <form [dynamic-form]=\"formConfig\"></form> `,\n})\nexport class MyFormComponent {\n transloco = inject(TranslocoService);\n\n formConfig = {\n // Define default validation messages for all fields\n defaultValidationMessages: {\n required: this.transloco.selectTranslate('validation.required'),\n email: this.transloco.selectTranslate('validation.email'),\n },\n fields: [\n {\n key: 'username',\n type: 'input',\n label: this.transloco.selectTranslate('form.username'),\n value: '',\n required: true,\n // Uses defaultValidationMessages for required\n },\n {\n key: 'email',\n type: 'input',\n label: this.transloco.selectTranslate('form.email'),\n value: '',\n required: true,\n email: true,\n // Uses defaultValidationMessages for required and email\n },\n ],\n };\n\n // Optional: Switch languages\n changeLanguage(lang: string) {\n this.transloco.setActiveLang(lang);\n // Form automatically updates due to reRenderOnLangChange: true\n }\n}\n```\n\n## Example with Signals\n\nUse Angular signals for translations by wrapping the config in `computed()`:\n\n```typescript\nimport { Component, signal, computed } from '@angular/core';\n\n@Component({...})\nexport class MyFormComponent {\n currentLang = signal<'en' | 'es'>('en');\n\n translations = computed(() => ({\n en: {\n username: 'Username',\n email: 'Email',\n required: 'This field is required',\n email_format: 'Please enter a valid email address',\n },\n es: {\n username: 'Nombre de usuario',\n email: 'Correo electr\xF3nico',\n required: 'Este campo es obligatorio',\n email_format: 'Por favor ingrese una direcci\xF3n de correo v\xE1lida',\n },\n }[this.currentLang()]));\n\n // Wrap entire config in computed() - rebuilds when language changes\n formConfig = computed(() => ({\n defaultValidationMessages: {\n required: this.translations().required,\n email: this.translations().email_format,\n },\n fields: [\n {\n key: 'username',\n type: 'input',\n label: this.translations().username,\n value: '',\n required: true,\n },\n {\n key: 'email',\n type: 'input',\n label: this.translations().email,\n value: '',\n required: true,\n email: true,\n },\n ],\n }));\n\n switchLanguage(lang: 'en' | 'es') {\n this.currentLang.set(lang);\n // Config recomputes \u2192 form updates with new translations\n }\n}\n```\n\n**How it works:** The `computed()` wrapper tracks the `currentLang` signal dependency. When `currentLang` changes, the entire config is recomputed with new translation values, and the form updates automatically.\n\n## Translated Select Options\n\nOptions also support `DynamicText`:\n\n```typescript\n{\n key: 'country',\n type: 'select',\n label: translationService.translate('form.country'),\n value: '',\n options: translationService.translate('countries').pipe(\n map(countries => [\n { value: 'us', label: countries.us },\n { value: 'es', label: countries.es },\n ])\n ),\n}\n```\n\n## Other Translation Libraries\n\nDynamic Forms works with any library that provides Observables or Signals:\n\n**ngx-translate:**\n\n```typescript\nlabel: this.translate.get('form.username'); // Observable<string>\n```\n\n**Custom service:**\n\n```typescript\nlabel: this.myTranslationService.translate('key'); // Observable<string> or Signal<string>\n```\n\nThe key is that your translation method returns `Observable<string>` or `Signal<string>`."
57
- },
58
- {
59
- id: "dynamic-behavior-submission",
60
- title: "Dynamic Behavior Submission",
61
- category: "dynamic-behavior",
62
- content: "Form submission in dynamic forms integrates with Angular Signal Forms' native `submit()` function, providing automatic button disabling, loading states, and server error handling.\n\n## Overview\n\nThe submission system provides:\n\n- Async form submission with automatic loading state\n- Server-side validation error handling\n- Automatic submit button disabling during submission\n- Configurable button disabled behavior\n- Custom disabled logic via `FormStateCondition`\n\n**Note**: The submission mechanism is **optional** - you can still handle submission manually via the `(submitted)` output if you prefer.\n\n## Basic Usage\n\nConfigure form submission with the `submission` property:\n\n```typescript\nimport { Component, inject } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { FormConfig, InferFormValue } from '@ng-forge/dynamic-forms';\n\nconst formFields = {\n fields: [\n { type: 'input', key: 'email', label: 'Email', value: '', required: true, email: true },\n { type: 'input', key: 'password', label: 'Password', value: '', required: true, props: { type: 'password' } },\n { type: 'submit', key: 'submit', label: 'Sign In' },\n ],\n} as const satisfies FormConfig;\n\ntype LoginForm = InferFormValue<typeof formFields.fields>;\n\n@Component({...})\nexport class LoginFormComponent {\n private http = inject(HttpClient);\n\n config = {\n ...formFields,\n submission: {\n action: (form: LoginForm) => this.http.post('/api/login', form),\n },\n };\n}\n```\n\n## SubmissionConfig\n\nThe `submission.action` function receives the typed form value and can return an Observable or Promise:\n\n| Return Value | Meaning |\n| ---------------------- | ------------------------------------------ |\n| Completes successfully | Successful submission |\n| `TreeValidationResult` | Server validation errors (single or array) |\n\n**Note:** `TreeValidationResult` from Angular Signal Forms supports both single errors and arrays. You can return a single error object or an array of errors.\n\nWhile the action is executing, the form is in a submitting state, enabling automatic button disabling and loading states.\n\n### Server Error Handling\n\nReturn validation errors to apply them to specific fields using `catchError`:\n\n```typescript\nimport { catchError, of } from 'rxjs';\n\nsubmission: {\n action: (form: FormValue) => this.http.post('/api/register', form).pipe(\n catchError((error) => {\n if (error.error?.code === 'EMAIL_EXISTS') {\n return of([{\n field: 'email',\n error: { kind: 'server', message: 'Email already exists' },\n }]);\n }\n throw error; // Re-throw unexpected errors\n }),\n ),\n}\n```\n\n## Button Disabled Behavior\n\nSubmit and next page buttons are automatically disabled based on form state. Configure defaults via `options`:\n\n```typescript\nconst config: FormConfig = {\n fields: [...],\n options: {\n submitButton: {\n disableWhenInvalid: true, // Disable when form is invalid (default: true)\n disableWhileSubmitting: true, // Disable during submission (default: true)\n },\n nextButton: {\n disableWhenPageInvalid: true, // Disable when current page is invalid (default: true)\n disableWhileSubmitting: true, // Disable during submission (default: true)\n }\n }\n};\n```\n\n### SubmitButtonOptions\n\n| Option | Type | Default | Description |\n| ------------------------ | --------- | ------- | ------------------------------ |\n| `disableWhenInvalid` | `boolean` | `true` | Disable when form is invalid |\n| `disableWhileSubmitting` | `boolean` | `true` | Disable during form submission |\n\n### NextButtonOptions\n\n| Option | Type | Default | Description |\n| ------------------------ | --------- | ------- | -------------------------------------------- |\n| `disableWhenPageInvalid` | `boolean` | `true` | Disable when current page has invalid fields |\n| `disableWhileSubmitting` | `boolean` | `true` | Disable during form submission |\n\n## Custom Button Logic\n\nOverride form-level defaults on individual buttons using the `logic` array:\n\n```typescript\n{\n type: 'submit',\n key: 'submit',\n label: 'Submit',\n logic: [\n { type: 'disabled', condition: 'formInvalid' },\n { type: 'disabled', condition: 'formSubmitting' }\n ]\n}\n```\n\n**Note:** `type: 'submit'` is a UI-integration convenience type that pre-configures the button with `SubmitEvent`. Use `type: 'button'` with `event: SubmitEvent` for the core API.\n\n### FormStateCondition\n\nSpecial condition strings for button logic:\n\n| Condition | Description |\n| ------------------ | ------------------------------------------------------------ |\n| `'formInvalid'` | Evaluates to `true` when the form is invalid |\n| `'formSubmitting'` | Evaluates to `true` when form submission is in progress |\n| `'pageInvalid'` | Evaluates to `true` when the current page has invalid fields |\n\n### Logic Precedence\n\nButton disabled state is determined in this order:\n\n1. **Explicit `disabled: true`** - Always wins\n2. **Field-level `logic` array** - If present, overrides form-level defaults\n3. **Form-level options** - Default behavior from `options.submitButton` or `options.nextButton`\n\n### Advanced Logic Example\n\nCombine form state conditions with custom expressions:\n\n```typescript\n{\n type: 'submit',\n key: 'submit',\n label: 'Submit',\n logic: [\n // Disable when form is invalid\n { type: 'disabled', condition: 'formInvalid' },\n // Disable when submitting\n { type: 'disabled', condition: 'formSubmitting' },\n // Also disable if terms not accepted\n {\n type: 'disabled',\n condition: {\n type: 'fieldValue',\n fieldPath: 'acceptTerms',\n operator: 'equals',\n value: false,\n }\n }\n ]\n}\n```\n\n## Value Exclusion\n\nBy default, field values are excluded from the `(submitted)` output when the field is hidden, disabled, or readonly. This prevents stale or irrelevant data from being submitted.\n\nValue exclusion supports a 3-tier configuration hierarchy: **Global > Form > Field**, where the most specific level wins. Internal form state and two-way binding are unaffected.\n\n```typescript\n// Disable hidden value exclusion for this form\nconst config: FormConfig = {\n fields: [...],\n options: {\n excludeValueIfHidden: false,\n }\n};\n```\n\nSee the **Value Exclusion** page under Recipes for full documentation on configuration tiers, per-field overrides, and migration instructions.\n\n## Manual Submission\n\nIf you prefer manual control, use the `(submitted)` output instead:\n\n```typescript\n@Component({\n template: `<form [dynamic-form]=\"config\" (submitted)=\"onSubmit($event)\"></form>`,\n})\nexport class MyFormComponent {\n onSubmit(value: FormValue) {\n this.http.post('/api/submit', value).subscribe();\n }\n}\n```"
63
- },
64
- {
65
- id: "dynamic-behavior-value-derivation-basics-async-derivation",
66
- title: "Dynamic Behavior Value Derivation Basics Async Derivation",
67
- category: "dynamic-behavior",
68
- content: "Derive field values from HTTP responses or custom async functions. Async derivations are ideal for server-driven computed values, address/zip lookups, exchange rates, and any value that requires a network call.\n\n## HTTP Derivations\n\nUse `source: 'http'` to derive a field value from an HTTP response. The request fires when declared dependencies change, with automatic debouncing and in-flight request cancellation.\n\n### Basic Example\n\n```typescript\n{\n key: 'exchangeRate',\n type: 'input',\n label: 'Exchange Rate',\n readonly: true,\n logic: [{\n type: 'derivation',\n source: 'http',\n http: {\n url: '/api/exchange-rate',\n method: 'GET',\n queryParams: {\n currency: 'formValue.currency',\n },\n },\n responseExpression: 'response.rate',\n dependsOn: ['currency'],\n }],\n}\n```\n\nWhen `currency` changes, a GET request is sent and `response.rate` becomes the new field value.\n\n### HttpRequestConfig\n\n| Property | Type | Required | Description |\n| ------------------------- | ------------------------- | -------- | --------------------------------------------------------------------------- |\n| `url` | `string` | Yes | Request URL. Use `:key` placeholders for path parameters |\n| `method` | `string` | No | HTTP method. Defaults to `'GET'` |\n| `params` | `Record<string, string>` | No | Path parameters. Values are expressions evaluated against the form context |\n| `queryParams` | `Record<string, string>` | No | Query parameters. Values are expressions evaluated against the form context |\n| `body` | `Record<string, unknown>` | No | Request body (for POST/PUT/PATCH) |\n| `evaluateBodyExpressions` | `boolean` | No | When `true`, top-level string values in `body` are treated as expressions |\n| `headers` | `Record<string, string>` | No | Request headers |\n\n### Path Parameters\n\nUse `:key` placeholders in the URL and provide values via `params`. Values are expressions evaluated against the form context, then `encodeURIComponent`-encoded:\n\n```typescript\nhttp: {\n url: '/api/users/:userId/orders/:orderId',\n params: {\n userId: 'formValue.userId',\n orderId: 'formValue.orderId',\n },\n}\n```\n\n### Query Params as Expressions\n\nValues in `queryParams` are evaluated as expressions, giving you dynamic request parameters built from form values:\n\n```typescript\nhttp: {\n url: '/api/shipping-estimate',\n queryParams: {\n fromZip: 'formValue.originZip',\n toZip: 'formValue.destinationZip',\n weight: 'formValue.packageWeight',\n },\n}\n```\n\n### POST with Dynamic Body\n\nFor `POST` requests, use `evaluateBodyExpressions: true` to build the body from form values:\n\n```typescript\nhttp: {\n url: '/api/calculate-tax',\n method: 'POST',\n body: {\n subtotal: 'formValue.subtotal',\n state: 'formValue.state',\n items: 'formValue.lineItems',\n },\n evaluateBodyExpressions: true,\n},\n```\n\nTop-level string values in `body` are evaluated as expressions. Nested objects are passed through as-is.\n\n### Extracting the Response Value\n\n`responseExpression` is evaluated with `{ response }` in scope and extracts the derived value:\n\n```typescript\nresponseExpression: 'response.rate'; // Simple property\nresponseExpression: 'response.data.price'; // Nested property\nresponseExpression: 'response.items[0].value'; // Array access\n```\n\n### Required Fields\n\n`source: 'http'` enforces two fields at the TypeScript level:\n\n- **`dependsOn: string[]`** \u2014 Explicit field dependencies. The request only re-fires when these fields change.\n- **`responseExpression: string`** \u2014 Expression to extract the derived value from the response body.\n\n### Controlling Request Timing\n\nUse `trigger` and `debounceMs` to control when requests fire:\n\n```typescript\n{\n type: 'derivation',\n source: 'http',\n http: {\n url: '/api/username-suggest',\n queryParams: { q: 'formValue.companyName' },\n },\n responseExpression: 'response.suggestion',\n dependsOn: ['companyName'],\n trigger: 'debounced',\n debounceMs: 400,\n}\n```\n\nThe request waits 400ms after the user stops typing before firing.\n\n### Conditional HTTP Derivation\n\nAdd a `condition` to only send the request when certain criteria are met:\n\n```typescript\n{\n key: 'suggestedPrice',\n type: 'input',\n readonly: true,\n logic: [{\n type: 'derivation',\n source: 'http',\n http: {\n url: '/api/price-suggest',\n queryParams: { productId: 'formValue.productId' },\n },\n responseExpression: 'response.suggestedPrice',\n dependsOn: ['productId'],\n condition: {\n type: 'fieldValue',\n fieldPath: 'productId',\n operator: 'notEquals',\n value: '',\n },\n }],\n}\n```\n\n## Async Function Derivations\n\nUse `source: 'asyncFunction'` when you need custom logic \u2014 Angular service injection, complex transformations, or any async computation not expressible as a plain HTTP config. Functions receive the full evaluation context and can return a `Promise` or `Observable`.\n\n### Basic Example\n\n```typescript\n// 1. Define and register the function in customFnConfig\nconst formConfig = {\n customFnConfig: {\n asyncDerivations: {\n lookupCity: async (context) => {\n const response = await fetch(`/api/geocode?zip=${context.formValue.zipCode}`);\n const data = await response.json();\n return data.city;\n },\n },\n },\n\n // 2. Reference it on the target field\n fields: [\n {\n key: 'city',\n type: 'input',\n label: 'City',\n readonly: true,\n logic: [\n {\n type: 'derivation',\n source: 'asyncFunction',\n asyncFunctionName: 'lookupCity',\n dependsOn: ['zipCode'],\n },\n ],\n },\n ],\n} as const satisfies FormConfig;\n```\n\n### Using RxJS Observables\n\nFunctions can return an `Observable` instead of a `Promise`, which is useful when using Angular services:\n\n```typescript\ncustomFnConfig: {\n asyncDerivations: {\n fetchAddress: (context) => {\n return inject(AddressService)\n .lookup(context.formValue.zipCode as string)\n .pipe(map(result => result.formattedAddress));\n },\n },\n},\n```\n\n### Available Context\n\nAsync derivation functions receive the full `EvaluationContext`:\n\n| Variable | Description |\n| ----------------------- | ------------------------------------------------ |\n| `context.formValue` | All current form field values |\n| `context.fieldValue` | The current value of the target field |\n| `context.externalData` | External data passed to the form (if configured) |\n| `context.rootFormValue` | Entire form value when inside array fields |\n\n### Conditional Async Derivation\n\nCombine with `condition` to only run the function when needed:\n\n```typescript\n{\n key: 'city',\n type: 'input',\n label: 'City',\n readonly: true,\n logic: [{\n type: 'derivation',\n source: 'asyncFunction',\n asyncFunctionName: 'lookupCity',\n dependsOn: ['zipCode'],\n condition: {\n type: 'fieldValue',\n fieldPath: 'enableLookup',\n operator: 'equals',\n value: true,\n },\n }],\n}\n```\n\nThe async function is only called when `enableLookup === true`.\n\n### Required Fields\n\n`source: 'asyncFunction'` requires:\n\n- **`asyncFunctionName: string`** \u2014 Name of the function registered in `customFnConfig.asyncDerivations`.\n- **`dependsOn: string[]`** \u2014 Explicit field dependencies. Without this, the function would re-run on every form change.\n\n## Stop On User Override\n\n`stopOnUserOverride` turns a derivation into a \"smart default\" \u2014 the field is auto-filled initially, but derivation stops once the user manually edits it.\n\n### Basic Example\n\n```typescript\n{\n key: 'displayName',\n type: 'input',\n label: 'Display Name',\n logic: [{\n type: 'derivation',\n expression: 'formValue.firstName + \" \" + formValue.lastName',\n stopOnUserOverride: true,\n }],\n}\n```\n\nThe display name is auto-filled from first and last name. Once the user changes it directly, auto-fill stops.\n\n### Re-Engaging on Dependency Change\n\nUse `reEngageOnDependencyChange: true` to clear the user override when a dependency changes. The derivation respects user edits, but re-engages if the underlying data changes:\n\n```typescript\n{\n key: 'phonePrefix',\n type: 'input',\n label: 'Phone Prefix',\n logic: [\n {\n type: 'derivation',\n value: '+1',\n condition: { type: 'fieldValue', fieldPath: 'country', operator: 'equals', value: 'US' },\n stopOnUserOverride: true,\n reEngageOnDependencyChange: true,\n dependsOn: ['country'],\n },\n {\n type: 'derivation',\n value: '+44',\n condition: { type: 'fieldValue', fieldPath: 'country', operator: 'equals', value: 'UK' },\n stopOnUserOverride: true,\n reEngageOnDependencyChange: true,\n dependsOn: ['country'],\n },\n ],\n}\n```\n\nWhen the user changes the country, the phone prefix is auto-filled again \u2014 overriding any previous manual edit.\n\n**Without** `reEngageOnDependencyChange`: once the user edits the field, the derivation never runs again for that field instance.\n\n**With** `reEngageOnDependencyChange`: the user override is cleared when a declared dependency changes, and the derivation re-fires.\n\n### Works with All Sources\n\n`stopOnUserOverride` and `reEngageOnDependencyChange` work across all derivation sources:\n\n```typescript\n// HTTP derivation with user override\n{\n type: 'derivation',\n source: 'http',\n http: {\n url: '/api/suggest-price',\n queryParams: { category: 'formValue.category' },\n },\n responseExpression: 'response.suggestedPrice',\n dependsOn: ['category'],\n stopOnUserOverride: true,\n reEngageOnDependencyChange: true,\n}\n\n// Async function derivation with user override\n{\n type: 'derivation',\n source: 'asyncFunction',\n asyncFunctionName: 'fetchDefaultTitle',\n dependsOn: ['template'],\n stopOnUserOverride: true,\n}\n```\n\n## DerivationLogicConfig Reference (Async)\n\n### HTTP Derivation Fields\n\n```typescript\n{\n type: 'derivation';\n source: 'http'; // Required \u2014 identifies HTTP mode\n\n http: HttpRequestConfig; // Required \u2014 request configuration\n dependsOn: string[]; // Required \u2014 explicit field dependencies\n responseExpression: string; // Required \u2014 expression to extract value from response\n\n // Shared optional fields\n trigger?: 'onChange' | 'debounced'; // Default: 'onChange'\n debounceMs?: number; // Default: 500 (when trigger: 'debounced')\n condition?: ConditionalExpression | boolean;\n stopOnUserOverride?: boolean;\n reEngageOnDependencyChange?: boolean;\n debugName?: string;\n}\n```\n\n### Async Function Derivation Fields\n\n```typescript\n{\n type: 'derivation';\n source: 'asyncFunction'; // Required \u2014 identifies async function mode\n\n asyncFunctionName: string; // Required \u2014 name in customFnConfig.asyncDerivations\n dependsOn: string[]; // Required \u2014 explicit field dependencies\n\n // Shared optional fields\n trigger?: 'onChange' | 'debounced';\n debounceMs?: number;\n condition?: ConditionalExpression | boolean;\n stopOnUserOverride?: boolean;\n reEngageOnDependencyChange?: boolean;\n debugName?: string;\n}\n```\n\n## Related\n\n- **[Values](./)** \u2014 Expression, static value, and function-based derivations\n- **[Properties](./property-derivation)** \u2014 Derive component properties from form values\n- **[HTTP Conditions](../../conditional-logic/overview/)** \u2014 HTTP-driven field visibility and state\n- **[Custom Validators](../../../validation/custom-validators/)** \u2014 Async and HTTP validation"
69
- },
70
- {
71
- id: "dynamic-behavior-value-derivation-basics",
72
- title: "Dynamic Behavior Value Derivation Basics",
73
- category: "dynamic-behavior",
74
- content: "Automatically compute and set field values based on other form values. Derivations enable calculated fields, auto-fill patterns, and value transformations.\n\n## Quick Start\n\nDerivations are defined ON the field that receives the computed value (self-targeting).\n\n### Shorthand Syntax (Preferred)\n\n```typescript\n{\n key: 'total',\n type: 'input',\n label: 'Total',\n readonly: true,\n derivation: 'formValue.quantity * formValue.unitPrice',\n}\n```\n\n### Logic Block Syntax\n\n```typescript\n{\n key: 'total',\n type: 'input',\n label: 'Total',\n readonly: true,\n logic: [{\n type: 'derivation',\n expression: 'formValue.quantity * formValue.unitPrice',\n }],\n}\n```\n\nWhen `quantity` or `unitPrice` changes, `total` is automatically recalculated.\n\n## Derivation Types\n\n### Expression-Based\n\nUse JavaScript expressions with access to `formValue`:\n\n```typescript\n{\n key: 'fullName',\n type: 'input',\n label: 'Full Name',\n readonly: true,\n derivation: '(formValue.firstName || \"\") + \" \" + (formValue.lastName || \"\")',\n}\n```\n\n**Available variables:**\n\n- `formValue` - Object containing all form field values\n- `externalData` - External application state (when configured in FormConfig)\n\n### Static Value\n\nSet a constant value when a condition is met:\n\n```typescript\n{\n key: 'phonePrefix',\n type: 'input',\n label: 'Phone Prefix',\n readonly: true,\n logic: [{\n type: 'derivation',\n value: '+1',\n condition: {\n type: 'fieldValue',\n fieldPath: 'country',\n operator: 'equals',\n value: 'USA',\n },\n }],\n}\n```\n\n### Custom Function\n\nUse a registered function for complex logic:\n\n```typescript\n// In form config\ncustomFnConfig: {\n derivations: {\n calculateTax: (ctx) => ctx.formValue.subtotal * getTaxRate(ctx.formValue.state),\n },\n},\nfields: [\n {\n key: 'tax',\n type: 'input',\n readonly: true,\n logic: [{\n type: 'derivation',\n functionName: 'calculateTax',\n dependsOn: ['subtotal', 'state'],\n }],\n },\n],\n```\n\n## Trigger Timing\n\nControl when derivations evaluate:\n\n| Trigger | Description | Use Case |\n| ----------- | ------------------------------------- | ------------------------------------- |\n| `onChange` | Immediately on value change (default) | Computed totals, conditional prefixes |\n| `debounced` | After value stabilizes | Self-transforms, format masking |\n\n### Debounced Derivations\n\nUse `trigger: 'debounced'` for self-transforming fields to avoid interrupting the user while typing:\n\n```typescript\n{\n key: 'email',\n type: 'input',\n label: 'Email',\n logic: [{\n type: 'derivation',\n expression: 'formValue.email.toLowerCase()',\n trigger: 'debounced',\n debounceMs: 500, // optional, defaults to 500\n }],\n}\n```\n\nThe transformation applies after the user stops typing for 500ms.\n\n## Conditional Derivations\n\nOnly apply derivations when conditions are met:\n\n```typescript\n{\n key: 'currency',\n type: 'input',\n label: 'Currency',\n readonly: true,\n logic: [\n {\n type: 'derivation',\n value: 'USD',\n condition: {\n type: 'fieldValue',\n fieldPath: 'country',\n operator: 'equals',\n value: 'USA',\n },\n },\n {\n type: 'derivation',\n value: 'EUR',\n condition: {\n type: 'fieldValue',\n fieldPath: 'country',\n operator: 'equals',\n value: 'Germany',\n },\n },\n ],\n}\n```\n\nMultiple derivations on the same field are evaluated in order.\n\n## Dependencies\n\n### Automatic Detection\n\nFor expressions, dependencies are automatically extracted:\n\n```typescript\n{\n key: 'total',\n type: 'input',\n label: 'Total',\n readonly: true,\n derivation: 'formValue.quantity * formValue.unitPrice',\n // Automatically depends on: quantity, unitPrice\n}\n```\n\n### Explicit Dependencies\n\nFor custom functions, specify dependencies explicitly:\n\n```typescript\n{\n key: 'discount',\n type: 'input',\n label: 'Discount',\n readonly: true,\n logic: [{\n type: 'derivation',\n functionName: 'calculateDiscount',\n dependsOn: ['total', 'memberLevel'],\n }],\n}\n```\n\nWithout `dependsOn`, custom functions re-evaluate on any form change.\n\n## Complete Example\n\n```typescript\nconst orderForm = {\n fields: [\n {\n key: 'quantity',\n type: 'input',\n value: 1,\n label: 'Quantity',\n props: { type: 'number' },\n },\n {\n key: 'unitPrice',\n type: 'input',\n value: 10,\n label: 'Unit Price',\n props: { type: 'number' },\n },\n {\n key: 'subtotal',\n type: 'input',\n value: 0,\n label: 'Subtotal',\n readonly: true,\n derivation: 'formValue.quantity * formValue.unitPrice',\n },\n {\n key: 'tax',\n type: 'input',\n value: 0,\n label: 'Tax (10%)',\n readonly: true,\n derivation: 'formValue.subtotal * 0.1',\n },\n {\n key: 'total',\n type: 'input',\n value: 0,\n label: 'Total',\n readonly: true,\n derivation: 'formValue.subtotal + formValue.tax',\n },\n ],\n} as const satisfies FormConfig;\n```\n\n## Array Field Derivations\n\nInside arrays, `formValue` is scoped to the current array item:\n\n```typescript\n{\n key: 'lineItems',\n type: 'array',\n fields: [\n {\n key: 'itemRow',\n type: 'row',\n fields: [\n { key: 'quantity', type: 'input', label: 'Qty', value: 1 },\n { key: 'unitPrice', type: 'input', label: 'Price', value: 0 },\n {\n key: 'lineTotal',\n type: 'input',\n label: 'Total',\n value: 0,\n readonly: true,\n // formValue is scoped to the current array item\n derivation: 'formValue.quantity * formValue.unitPrice',\n },\n ],\n },\n ],\n}\n```\n\n### Accessing Root Form Value\n\nInside array items, `formValue` refers to the current array item. Use `rootFormValue` to access the entire form:\n\n```typescript\n{\n key: 'lineTotal',\n type: 'input',\n label: 'Total',\n readonly: true,\n // formValue = current array item { quantity, unitPrice }\n // rootFormValue = entire form { lineItems, discount, ... }\n derivation: 'formValue.quantity * formValue.unitPrice * (1 - rootFormValue.discount / 100)',\n}\n```\n\n## Debugging Derivations\n\nEnable derivation logging to troubleshoot issues:\n\n### Configuration\n\nConfigure derivation logging via `withLoggerConfig`:\n\n```typescript\n// In your providers\nprovideDynamicForm(...withMaterialFields(), withLoggerConfig({ derivations: 'verbose' }));\n\n// Or just summary level\nprovideDynamicForm(...withMaterialFields(), withLoggerConfig({ derivations: 'summary' }));\n```\n\n### Log Levels\n\n| Level | Output |\n| --------- | --------------------------------- |\n| `none` | No logging (default) |\n| `summary` | Cycle completion with counts |\n| `verbose` | Individual derivation evaluations |\n\n### Using debugName\n\nAdd names to derivations for easier identification in logs:\n\n```typescript\n{\n key: 'lineTotal',\n type: 'input',\n label: 'Total',\n readonly: true,\n logic: [{\n type: 'derivation',\n debugName: 'Calculate line total',\n expression: 'formValue.quantity * formValue.unitPrice',\n }],\n}\n```\n\n**Console output (verbose mode):**\n\n```\nDerivation - Starting cycle (onChange) with 5 derivation(s)\nDerivation - Iteration 1\nDerivation - Applied \"Calculate line total\" { field: 'lineTotal', newValue: 150 }\nDerivation - Skipped: phonePrefix (condition not met)\nDerivation - Cycle complete (onChange) { applied: 1, skipped: 4, errors: 0, iterations: 1 }\n```\n\n## Bidirectional Derivations\n\nCreate two-way bindings between fields:\n\n```typescript\n// Celsius to Fahrenheit\n{\n key: 'fahrenheit',\n type: 'input',\n value: 32,\n derivation: 'formValue.celsius * 9 / 5 + 32',\n}\n\n// Fahrenheit to Celsius\n{\n key: 'celsius',\n type: 'input',\n value: 0,\n derivation: '(formValue.fahrenheit - 32) * 5 / 9',\n}\n```\n\n### Cycle Detection\n\nThe system automatically detects derivation cycles and warns during development:\n\n```\nBidirectional derivation detected: celsius <-> fahrenheit\nBidirectional derivations stabilize via equality check.\n```\n\n### Floating-Point Precision\n\nBidirectional derivations stabilize when the computed value equals the current value. For floating-point operations, consider:\n\n1. **Rounding in expressions:**\n\n ```typescript\n derivation: 'Math.round(formValue.usd * exchangeRate * 100) / 100';\n ```\n\n2. **Using integers:** Store cents instead of dollars\n3. **One-way derivation:** If bidirectional isn't required\n\n## Derivation Processing\n\n### Evaluation Order\n\nDerivations are topologically sorted based on dependencies:\n\n```\nquantity \u2500\u2510\n \u251C\u2500\u2500 subtotal \u2500\u2510\nunitPrice \u2500\u2518 \u251C\u2500\u2500 total\n \u2502\n tax \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\nThis ensures `subtotal` is computed before `total`.\n\n### Iteration Limits\n\nTo prevent infinite loops, derivations are limited to 10 iterations per cycle. If exceeded:\n\n```\nDerivation - Max iterations reached (onChange).\nThis may indicate a loop in derivation logic.\n```\n\n## DerivationLogicConfig Interface\n\n```typescript\ninterface DerivationLogicConfig {\n /** Logic type identifier */\n type: 'derivation';\n\n /** Optional name for debugging */\n debugName?: string;\n\n /** When to evaluate: 'onChange' (default) or 'debounced' */\n trigger?: 'onChange' | 'debounced';\n\n /** Debounce duration in ms (default: 500) */\n debounceMs?: number;\n\n /** Static value to set */\n value?: unknown;\n\n /** JavaScript expression (has access to formValue) */\n expression?: string;\n\n /** Name of registered custom function */\n functionName?: string;\n\n /** Explicit field dependencies */\n dependsOn?: string[];\n\n /** Condition for when derivation applies */\n condition?: ConditionalExpression | boolean;\n}\n```\n\n## External Data in Derivations\n\nUse external application state in derivation expressions:\n\n```typescript\nconst config = {\n externalData: {\n discountRate: computed(() => pricingService.currentDiscount()),\n },\n fields: [\n {\n key: 'discountedPrice',\n type: 'input',\n label: 'Discounted Price',\n readonly: true,\n derivation: 'formValue.price * (1 - externalData.discountRate)',\n },\n ],\n} as const satisfies FormConfig;\n```\n\nExternal data values are reactively tracked - when signals change, derivations are re-evaluated.\n\n## Related\n\n- **Property Derivation** (see tab above) \u2014 Derive component properties (minDate, options, label) from form values\n- **Async Derivation** (see tab above) \u2014 HTTP and async function derivations, stopOnUserOverride\n- **[Conditional Logic](../conditional-logic/overview/)** \u2014 Control field visibility and state\n- **[Array Fields](../../prebuilt/form-arrays/simplified/)** \u2014 Working with array fields\n- **[Examples](/examples)** \u2014 Real-world form patterns"
75
- },
76
- {
77
- id: "dynamic-behavior-value-derivation-basics-property-derivation",
78
- title: "Dynamic Behavior Value Derivation Basics Property Derivation",
79
- category: "dynamic-behavior",
80
- content: "Reactively derive field component properties (like `minDate`, `options`, `label`, `placeholder`) based on form values. While value derivations set a field's form value, property derivations set **component input properties**.\n\n## Quick Start\n\nProperty derivations use `type: 'derivation'` with `targetProperty` in the `logic` array and are self-targeting: the logic is placed on the field whose property should be derived.\n\n```typescript\n{\n key: 'endDate',\n type: 'datepicker',\n label: 'End Date',\n logic: [{\n type: 'derivation',\n targetProperty: 'minDate',\n expression: 'formValue.startDate',\n }]\n}\n```\n\nWhen `startDate` changes, the `minDate` property on `endDate`'s datepicker component is automatically updated.\n\n> **Migration note:** The previous `type: 'propertyDerivation'` syntax is deprecated but still works.\n> Use `type: 'derivation'` with `targetProperty` instead. Similarly, register functions via\n> `customFnConfig.derivations` instead of `customFnConfig.propertyDerivations`.\n\n## Derivation Sources\n\nProperty derivations support three mutually exclusive ways to compute a value.\n\n### Expression-Based\n\nUse JavaScript expressions with access to `formValue`:\n\n```typescript\n{\n key: 'endDate',\n type: 'datepicker',\n label: 'End Date',\n logic: [{\n type: 'derivation',\n targetProperty: 'minDate',\n expression: 'formValue.startDate',\n }]\n}\n```\n\n**Available variables:**\n\n- `formValue` - Object containing all form field values\n- `externalData` - External application state (when configured in FormConfig)\n\n### Static Value\n\nSet a constant property value when a condition is met:\n\n```typescript\n{\n key: 'phone',\n type: 'input',\n label: 'Phone',\n logic: [{\n type: 'derivation',\n targetProperty: 'label',\n value: 'Mobile Phone',\n condition: {\n type: 'fieldValue',\n fieldPath: 'contactType',\n operator: 'equals',\n value: 'mobile',\n },\n }]\n}\n```\n\n### Custom Function\n\nUse a registered function for complex logic:\n\n```typescript\n// In form config\ncustomFnConfig: {\n derivations: {\n getCitiesForCountry: (ctx) => {\n const cities: Record<string, { label: string; value: string }[]> = {\n 'US': [{ label: 'New York', value: 'nyc' }, { label: 'LA', value: 'la' }],\n 'DE': [{ label: 'Berlin', value: 'berlin' }],\n };\n return cities[ctx.formValue.country as string] ?? [];\n },\n },\n},\nfields: [\n {\n key: 'city',\n type: 'select',\n label: 'City',\n options: [],\n logic: [{\n type: 'derivation',\n targetProperty: 'options',\n functionName: 'getCitiesForCountry',\n dependsOn: ['country'],\n }],\n },\n],\n```\n\n## Target Properties\n\n### Simple Properties\n\nSet any direct component input property:\n\n```typescript\ntargetProperty: 'minDate'; // Date constraint\ntargetProperty: 'maxDate'; // Date constraint\ntargetProperty: 'options'; // Select/radio options\ntargetProperty: 'label'; // Field label\ntargetProperty: 'placeholder'; // Input placeholder\ntargetProperty: 'hint'; // Hint text\ntargetProperty: 'rows'; // Textarea rows\n```\n\n### Nested Properties (Dot Notation)\n\nSet properties nested one level deep using dot notation:\n\n```typescript\ntargetProperty: 'props.appearance'; // Material appearance\ntargetProperty: 'props.color'; // Material color\ntargetProperty: 'meta.autocomplete'; // Custom metadata\n```\n\n> **Note:** Maximum nesting depth is 2 levels (one dot). Deeper paths like `props.nested.deep` are not supported.\n\n## Trigger Timing\n\nControl when property derivations evaluate:\n\n| Trigger | Description | Use Case |\n| ----------- | ------------------------------------- | --------------------------------- |\n| `onChange` | Immediately on value change (default) | Date constraints, dynamic options |\n| `debounced` | After value stabilizes | Expensive lookups, search queries |\n\n### Debounced Property Derivations\n\nUse `trigger: 'debounced'` for expensive operations:\n\n```typescript\n{\n key: 'productSearch',\n type: 'select',\n label: 'Product',\n options: [],\n logic: [{\n type: 'derivation',\n targetProperty: 'options',\n functionName: 'searchProducts',\n trigger: 'debounced',\n debounceMs: 300,\n dependsOn: ['searchQuery'],\n }]\n}\n```\n\n## Conditional Property Derivations\n\nOnly apply property derivations when conditions are met:\n\n```typescript\n{\n key: 'email',\n type: 'input',\n label: 'Email',\n logic: [\n {\n type: 'derivation',\n targetProperty: 'label',\n value: 'Work Email',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n },\n {\n type: 'derivation',\n targetProperty: 'label',\n value: 'Personal Email',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'personal',\n },\n },\n ],\n}\n```\n\n## Dependencies\n\n### Automatic Detection\n\nFor expressions, dependencies are automatically extracted:\n\n```typescript\n{\n key: 'endDate',\n type: 'datepicker',\n label: 'End Date',\n logic: [{\n type: 'derivation',\n targetProperty: 'minDate',\n expression: 'formValue.startDate',\n // Automatically depends on: startDate\n }]\n}\n```\n\n### Explicit Dependencies\n\nFor custom functions, specify dependencies explicitly:\n\n```typescript\n{\n key: 'city',\n type: 'select',\n label: 'City',\n options: [],\n logic: [{\n type: 'derivation',\n targetProperty: 'options',\n functionName: 'getCitiesForCountry',\n dependsOn: ['country'],\n }]\n}\n```\n\nWithout `dependsOn`, custom functions re-evaluate on **any** form value change.\n\n## Array Field Property Derivations\n\nInside arrays, `formValue` is scoped to the current array item:\n\n```typescript\n{\n key: 'lineItems',\n type: 'array',\n fields: [\n {\n key: 'item',\n type: 'group',\n fields: [\n { key: 'startDate', type: 'datepicker', label: 'Start' },\n {\n key: 'endDate',\n type: 'datepicker',\n label: 'End',\n logic: [{\n type: 'derivation',\n targetProperty: 'minDate',\n // formValue is scoped to the current array item\n expression: 'formValue.startDate',\n }]\n },\n ],\n },\n ],\n}\n```\n\nEach array item independently derives its own `endDate.minDate` from its own `startDate`.\n\n## Complete Example\n\n```typescript\nconst travelForm = {\n customFnConfig: {\n derivations: {\n getCitiesForCountry: (ctx) => {\n const cities: Record<string, { label: string; value: string }[]> = {\n US: [\n { label: 'New York', value: 'nyc' },\n { label: 'Los Angeles', value: 'la' },\n { label: 'Chicago', value: 'chi' },\n ],\n DE: [\n { label: 'Berlin', value: 'berlin' },\n { label: 'Munich', value: 'munich' },\n ],\n };\n return cities[ctx.formValue.country as string] ?? [];\n },\n },\n },\n fields: [\n {\n key: 'country',\n type: 'select',\n label: 'Country',\n required: true,\n options: [\n { label: 'USA', value: 'US' },\n { label: 'Germany', value: 'DE' },\n ],\n },\n {\n key: 'city',\n type: 'select',\n label: 'City',\n required: true,\n options: [],\n logic: [\n {\n type: 'derivation',\n targetProperty: 'options',\n functionName: 'getCitiesForCountry',\n dependsOn: ['country'],\n },\n ],\n },\n { key: 'startDate', type: 'datepicker', label: 'Travel Start', required: true },\n {\n key: 'endDate',\n type: 'datepicker',\n label: 'Travel End',\n required: true,\n logic: [\n {\n type: 'derivation',\n targetProperty: 'minDate',\n expression: 'formValue.startDate',\n },\n ],\n },\n {\n key: 'notes',\n type: 'textarea',\n label: 'Notes',\n logic: [\n {\n type: 'derivation',\n targetProperty: 'props.appearance',\n value: 'fill',\n condition: {\n type: 'fieldValue',\n fieldPath: 'country',\n operator: 'equals',\n value: 'DE',\n },\n },\n {\n type: 'derivation',\n targetProperty: 'props.appearance',\n value: 'outline',\n condition: {\n type: 'fieldValue',\n fieldPath: 'country',\n operator: 'notEquals',\n value: 'DE',\n },\n },\n ],\n },\n { key: 'submit', type: 'submit', label: 'Book Trip' },\n ],\n} as const satisfies FormConfig;\n```\n\n## Comparison with Value Derivation\n\n| Aspect | Value Derivation | Property Derivation |\n| ---------------- | ---------------------------- | --------------------------------------- |\n| Logic type | `type: 'derivation'` | `type: 'derivation'` + `targetProperty` |\n| Sets | Field's form value | Component input property |\n| Target | Implicit (self) | `targetProperty: 'minDate'` |\n| Shorthand | `derivation: 'expr'` | None (must use logic block) |\n| Chaining | Topologically sorted | No chaining (single pass) |\n| Custom functions | `customFnConfig.derivations` | `customFnConfig.derivations` |\n| Max iterations | Configurable (default 10) | Single pass |\n\n## Debugging\n\nAdd `debugName` to property derivations for easier identification in logs:\n\n```typescript\nlogic: [\n {\n type: 'derivation',\n debugName: 'endDate minDate constraint',\n targetProperty: 'minDate',\n expression: 'formValue.startDate',\n },\n];\n```\n\n## DerivationLogicConfig Interface\n\nBoth value derivations and property derivations use the unified `DerivationLogicConfig`. The presence of `targetProperty` is what distinguishes a property derivation from a value derivation.\n\n```typescript\ninterface DerivationLogicConfig {\n /** Logic type identifier (unified for both value and property derivations) */\n type: 'derivation';\n\n /** Property to set on the field component. When present, this is a property derivation. When absent, this is a value derivation. */\n targetProperty?: string;\n\n /** Optional name for debugging */\n debugName?: string;\n\n /** When to evaluate: 'onChange' (default) or 'debounced' */\n trigger?: 'onChange' | 'debounced';\n\n /** Debounce duration in ms (default: 500) */\n debounceMs?: number;\n\n /** Static value to set (mutually exclusive) */\n value?: unknown;\n\n /** JavaScript expression (mutually exclusive) */\n expression?: string;\n\n /** Name of registered custom function (mutually exclusive) */\n functionName?: string;\n\n /** Explicit field dependencies */\n dependsOn?: string[];\n\n /** Condition for when derivation applies (default: true) */\n condition?: ConditionalExpression | boolean;\n}\n```\n\n## External Data in Property Derivations\n\nUse external application state in property derivation custom functions:\n\n```typescript\nconst config = {\n externalData: {\n userRegion: computed(() => this.regionService.current()),\n },\n customFnConfig: {\n derivations: {\n getCurrencyOptions: (ctx) => {\n const optionsByRegion: Record<string, { label: string; value: string }[]> = {\n EU: [\n { label: 'EUR', value: 'eur' },\n { label: 'GBP', value: 'gbp' },\n ],\n US: [\n { label: 'USD', value: 'usd' },\n { label: 'CAD', value: 'cad' },\n ],\n };\n return optionsByRegion[ctx.externalData.userRegion as string] ?? [];\n },\n },\n },\n fields: [\n {\n key: 'currency',\n type: 'select',\n label: 'Currency',\n options: [],\n logic: [\n {\n type: 'derivation',\n targetProperty: 'options',\n functionName: 'getCurrencyOptions',\n },\n ],\n },\n ],\n} as const satisfies FormConfig;\n```\n\nExternal data values are reactively tracked - when signals change, property derivations are re-evaluated.\n\n## Related\n\n- **Value Derivation** (see tab above) - Compute field form values\n- **[Conditional Logic](../conditional-logic/overview/)** - Control field visibility and state\n- **[Array Fields](../../prebuilt/form-arrays/simplified/)** - Working with array fields"
81
- },
82
- {
83
- id: "age-conditional-form",
84
- title: "Age Conditional Form",
85
- category: "examples",
86
- content: "[\u2190 Back to Quick Start](/examples)\n\nRegistration form demonstrating numeric comparison operators for age-based conditional logic.\n\n## Live Demo\n\n<iframe src=\"http://localhost:4201/#/examples/age-conditional-form\" class=\"example-frame\" title=\"Age Conditional Form Demo\"></iframe>\n\n## Overview\n\nThis example shows how to use numeric comparison operators to display different fields based on age ranges. Minors see parental consent fields, while seniors see discount options.\n\n**Key patterns demonstrated:**\n\n- Age-appropriate field display\n- Numeric comparisons (greater than, less than)\n- Multiple conditional sections based on same field\n\n## Implementation\n\n```typescript\nimport { Component, signal } from '@angular/core';\nimport { DynamicForm, type FormConfig } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-age-conditional-form',\n imports: [DynamicForm],\n template: `<form [dynamic-form]=\"config\" [(value)]=\"formValue\"></form>`,\n})\nexport class AgeConditionalFormComponent {\n formValue = signal({});\n\n config = {\n fields: [\n {\n key: 'name',\n type: 'input',\n value: '',\n label: 'Full Name',\n required: true,\n },\n {\n key: 'email',\n type: 'input',\n value: '',\n label: 'Email Address',\n required: true,\n email: true,\n },\n {\n key: 'age',\n type: 'input',\n value: null,\n label: 'Age',\n required: true,\n min: 0,\n max: 120,\n props: {\n type: 'number',\n hint: 'Enter your age to see relevant options',\n },\n },\n {\n key: 'parentalConsent',\n type: 'checkbox',\n value: false,\n label: 'I have parental/guardian consent to register',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'age',\n operator: 'greaterOrEqual',\n value: 18,\n },\n },\n {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'age',\n operator: 'less',\n value: 18,\n },\n },\n ],\n },\n {\n key: 'parentEmail',\n type: 'input',\n value: '',\n label: 'Parent/Guardian Email',\n email: true,\n props: {\n hint: 'We will send a verification email to your parent/guardian',\n },\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'age',\n operator: 'greaterOrEqual',\n value: 18,\n },\n },\n {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'age',\n operator: 'less',\n value: 18,\n },\n },\n ],\n },\n {\n key: 'seniorDiscount',\n type: 'checkbox',\n value: false,\n label: 'Apply senior discount (65+)',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'age',\n operator: 'less',\n value: 65,\n },\n },\n ],\n },\n {\n key: 'aarpMember',\n type: 'checkbox',\n value: false,\n label: 'I am an AARP member (additional 5% discount)',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'age',\n operator: 'less',\n value: 65,\n },\n },\n ],\n },\n {\n type: 'submit',\n key: 'submit',\n label: 'Register',\n },\n ],\n } as const satisfies FormConfig;\n}\n```\n\n## How It Works\n\n### Age-Based Visibility\n\nFields for minors are hidden when age >= 18:\n\n```typescript\n{\n key: 'parentalConsent',\n type: 'checkbox',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'age',\n operator: 'greaterOrEqual',\n value: 18,\n },\n }],\n}\n```\n\nFields for seniors are hidden when age < 65:\n\n```typescript\n{\n key: 'seniorDiscount',\n type: 'checkbox',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'age',\n operator: 'less',\n value: 65,\n },\n }],\n}\n```\n\n### Conditional Required for Minors\n\nParent email is required only for minors:\n\n```typescript\n{\n key: 'parentEmail',\n type: 'input',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'age',\n operator: 'greaterOrEqual',\n value: 18,\n },\n },\n {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'age',\n operator: 'less',\n value: 18,\n },\n },\n ],\n}\n```\n\n## Comparison Operators\n\nThis example uses numeric comparison operators:\n\n| Operator | Description |\n| ---------------- | -------------------------- |\n| `greater` | Greater than (>) |\n| `less` | Less than (<) |\n| `greaterOrEqual` | Greater than or equal (>=) |\n| `lessOrEqual` | Less than or equal (<=) |\n\n## Use Cases\n\n- Age-gated registration\n- Tiered pricing forms\n- Compliance forms with age requirements\n- Membership applications\n\n## Related Documentation\n\n- **[Conditional Logic](../../dynamic-behavior/conditional-logic/overview/)** - Full conditional logic guide\n- **[Validation](../../validation/basics/)** - Min/max validation\n- **[User Registration](../user-registration/)** - Basic registration example"
87
- },
88
- {
89
- id: "array-form",
90
- title: "Array Form",
91
- category: "examples",
92
- content: "[\u2190 Back to Quick Start](/examples)\n\nDynamic form arrays using the complete API with declarative add/remove buttons, custom button placement, and full control over item structure.\n\n## Live Demo\n\n<iframe src=\"http://localhost:4201/#/examples/array\" class=\"example-frame\" title=\"Array Field Demo\"></iframe>\n\n## Overview\n\nThis example demonstrates the complete array API with:\n\n- **Flat arrays** with row-based items (input + remove button)\n- **Object arrays** with multiple fields per item\n- **Declarative buttons** for add, prepend, and remove operations\n- **Custom button placement** outside the array container\n- **Template-based item creation**\n\n## Implementation\n\n```typescript\nimport { Component, signal } from '@angular/core';\nimport { DynamicForm, FormConfig } from '@ng-forge/dynamic-forms';\n\n// Template for new tags \u2014 a row with an input and remove button\nconst tagTemplate = {\n key: 'tag',\n type: 'row',\n fields: [\n {\n key: 'value',\n type: 'input',\n label: 'Tag',\n required: true,\n minLength: 2,\n props: { placeholder: 'Enter a tag', hint: 'Tags must be at least 2 characters' },\n },\n {\n key: 'removeTag',\n type: 'removeArrayItem',\n label: 'Remove',\n className: 'remove-tag-button',\n props: { color: 'warn' },\n },\n ],\n} as const;\n\n// Template for new contacts \u2014 includes remove button inside each item\nconst contactTemplate = [\n {\n key: 'name',\n type: 'input',\n label: 'Contact Name',\n required: true,\n minLength: 2,\n props: { placeholder: 'Enter contact name', hint: 'Full name of the emergency contact' },\n },\n {\n key: 'phone',\n type: 'input',\n label: 'Phone Number',\n required: true,\n pattern: /^\\d{10}$/,\n validationMessages: { pattern: 'Please enter a valid 10-digit phone number' },\n props: { type: 'tel', placeholder: '5551234567' },\n },\n {\n key: 'relationship',\n type: 'select',\n label: 'Relationship',\n required: true,\n options: [\n { label: 'Family', value: 'family' },\n { label: 'Friend', value: 'friend' },\n { label: 'Colleague', value: 'colleague' },\n { label: 'Other', value: 'other' },\n ],\n },\n {\n key: 'removeContact',\n type: 'removeArrayItem',\n label: 'Remove Contact',\n className: 'remove-contact-button',\n props: { color: 'warn' },\n },\n] as const;\n\n@Component({\n selector: 'app-array-form',\n imports: [DynamicForm],\n template: `<form [dynamic-form]=\"config\" [(value)]=\"formValue\"></form>`,\n})\nexport class ArrayFormComponent {\n formValue = signal({});\n\n config = {\n fields: [\n // Flat array with row-based items\n {\n key: 'tags',\n type: 'array',\n fields: [\n [\n {\n key: 'tag',\n type: 'row',\n fields: [\n { key: 'value', type: 'input', label: 'Tag', value: 'angular', required: true, minLength: 2 },\n { key: 'removeTag', type: 'removeArrayItem', label: 'Remove', props: { color: 'warn' } },\n ],\n },\n ],\n [\n {\n key: 'tag',\n type: 'row',\n fields: [\n { key: 'value', type: 'input', label: 'Tag', value: 'typescript', required: true, minLength: 2 },\n { key: 'removeTag', type: 'removeArrayItem', label: 'Remove', props: { color: 'warn' } },\n ],\n },\n ],\n ],\n },\n // Add button placed outside the array\n {\n key: 'addTagButton',\n type: 'addArrayItem',\n label: 'Add Tag',\n arrayKey: 'tags',\n template: [tagTemplate],\n props: { color: 'primary' },\n },\n\n // Object array with multiple fields per item\n {\n key: 'contacts',\n type: 'array',\n fields: [\n [\n { key: 'name', type: 'input', label: 'Contact Name', value: 'Jane Smith', required: true, minLength: 2 },\n { key: 'phone', type: 'input', label: 'Phone Number', value: '5551234567', required: true },\n {\n key: 'relationship',\n type: 'select',\n label: 'Relationship',\n value: 'family',\n required: true,\n options: [\n { label: 'Family', value: 'family' },\n { label: 'Friend', value: 'friend' },\n { label: 'Colleague', value: 'colleague' },\n { label: 'Other', value: 'other' },\n ],\n },\n { key: 'removeContact', type: 'removeArrayItem', label: 'Remove Contact', props: { color: 'warn' } },\n ],\n ],\n },\n // Multiple add buttons with different behaviors\n {\n key: 'contactButtons',\n type: 'row',\n fields: [\n {\n key: 'prependContactButton',\n type: 'prependArrayItem',\n label: 'Add First',\n arrayKey: 'contacts',\n template: contactTemplate,\n props: { color: 'accent' },\n },\n {\n key: 'addContactButton',\n type: 'addArrayItem',\n label: 'Add Contact',\n arrayKey: 'contacts',\n template: contactTemplate,\n props: { color: 'primary' },\n },\n ],\n },\n\n { key: 'submit', type: 'submit', label: 'Save All', props: { color: 'primary' } },\n ],\n } as const satisfies FormConfig;\n}\n```\n\n## Key Features\n\n### Declarative Add/Remove Buttons\n\nUse `addArrayItem`, `prependArrayItem`, and `removeArrayItem` button types directly in your config:\n\n```typescript\n// Add button outside the array\n{\n key: 'addTag',\n type: 'addArrayItem',\n label: 'Add Tag',\n arrayKey: 'tags', // Points to the array field\n template: tagTemplate, // Defines the new item structure\n}\n\n// Remove button inside each array item\n{\n key: 'removeTag',\n type: 'removeArrayItem',\n label: 'Remove',\n}\n```\n\n### Custom Button Placement\n\nPlace add/prepend buttons anywhere in the form layout:\n\n```typescript\n{\n key: 'contactButtons',\n type: 'row',\n fields: [\n { key: 'prepend', type: 'prependArrayItem', label: 'Add First', arrayKey: 'contacts', template: contactTemplate },\n { key: 'append', type: 'addArrayItem', label: 'Add Last', arrayKey: 'contacts', template: contactTemplate },\n ],\n}\n```\n\n### Templates for New Items\n\nEach add button defines its own template for what to insert:\n\n```typescript\nconst contactTemplate = [\n { key: 'name', type: 'input', label: 'Name' },\n { key: 'phone', type: 'input', label: 'Phone' },\n { key: 'remove', type: 'removeArrayItem', label: 'Remove' },\n];\n```\n\n## Use Cases\n\n- Forms requiring custom button placement\n- Arrays with multiple add operations (append, prepend, insert)\n- Heterogeneous arrays with different item structures\n- Complex layouts with row-based array items\n\n## Related Examples\n\n- **[Simplified Array Form](../simplified-array-form/)** - Simpler API with auto-generated buttons\n- **[Paginated Form](../paginated-form/)** - Multi-step wizard form\n- **[Contact Form](../contact-form/)** - Basic contact form\n\n## Related Documentation\n\n- **[Complete Array API](../../prebuilt/form-arrays/complete/)** - Full API reference\n- **[Simplified Array API](../../prebuilt/form-arrays/simplified/)** - Simpler alternative\n- **[Validation](../../validation/basics/)** - Form validation guide"
93
- },
94
- {
95
- id: "business-account-form",
96
- title: "Business Account Form",
97
- category: "examples",
98
- content: "[\u2190 Back to Quick Start](/examples)\n\nAccount registration form that shows different fields based on whether the user selects personal or business account type.\n\n## Live Demo\n\n<iframe src=\"http://localhost:4201/#/examples/business-account-form\" class=\"example-frame\" title=\"Business Account Form Demo\"></iframe>\n\n## Overview\n\nThis example demonstrates how to conditionally show business-specific fields only when the user selects a business account type.\n\n**Key patterns demonstrated:**\n\n- Personal name field hidden for business accounts\n- Business fields (company name, tax ID) hidden for personal accounts\n- Required validation tied to visibility\n\n## Implementation\n\n```typescript\nimport { Component, signal } from '@angular/core';\nimport { DynamicForm, type FormConfig } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-business-account-form',\n imports: [DynamicForm],\n template: `<form [dynamic-form]=\"config\" [(value)]=\"formValue\"></form>`,\n})\nexport class BusinessAccountFormComponent {\n formValue = signal({});\n\n config = {\n fields: [\n {\n key: 'accountType',\n type: 'radio',\n value: 'personal',\n label: 'Account Type',\n required: true,\n options: [\n { value: 'personal', label: 'Personal Account' },\n { value: 'business', label: 'Business Account' },\n ],\n },\n {\n key: 'name',\n type: 'input',\n value: '',\n label: 'Full Name',\n required: true,\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n },\n ],\n },\n {\n key: 'companyName',\n type: 'input',\n value: '',\n label: 'Company Name',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'notEquals',\n value: 'business',\n },\n },\n {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n },\n ],\n },\n {\n key: 'taxId',\n type: 'input',\n value: '',\n label: 'Tax ID',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'notEquals',\n value: 'business',\n },\n },\n {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n },\n ],\n },\n {\n key: 'numberOfEmployees',\n type: 'input',\n value: null,\n label: 'Number of Employees',\n props: { type: 'number' },\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'notEquals',\n value: 'business',\n },\n },\n ],\n },\n {\n key: 'email',\n type: 'input',\n value: '',\n label: 'Email Address',\n required: true,\n email: true,\n },\n {\n type: 'submit',\n key: 'submit',\n label: 'Create Account',\n },\n ],\n } as const satisfies FormConfig;\n}\n```\n\n## How It Works\n\n### Account Type Toggle\n\nThe form uses radio buttons to switch between personal and business accounts:\n\n```typescript\n{\n key: 'accountType',\n type: 'radio',\n value: 'personal',\n options: [\n { value: 'personal', label: 'Personal Account' },\n { value: 'business', label: 'Business Account' },\n ],\n}\n```\n\n### Personal vs Business Fields\n\nPersonal fields are hidden when business is selected:\n\n```typescript\n{\n key: 'name',\n type: 'input',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n }],\n}\n```\n\nBusiness fields are hidden when personal is selected:\n\n```typescript\n{\n key: 'companyName',\n type: 'input',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'notEquals',\n value: 'business',\n },\n }],\n}\n```\n\n## Use Cases\n\n- User registration with account types\n- Subscription forms with tiers\n- Customer onboarding flows\n- Multi-tenant application signup\n\n## Related Documentation\n\n- **[Conditional Logic](../../dynamic-behavior/conditional-logic/overview/)** - Full conditional logic guide\n- **[User Registration](../user-registration/)** - Basic registration example\n- **[Radio Buttons](../../schema-fields/field-types/)** - Field types reference"
99
- },
100
- {
101
- id: "contact-dynamic-fields",
102
- title: "Contact Dynamic Fields",
103
- category: "examples",
104
- content: "[\u2190 Back to Quick Start](/examples)\n\nContact form demonstrating dynamic field visibility based on preferred contact method selection.\n\n## Live Demo\n\n<iframe src=\"http://localhost:4201/#/examples/contact-dynamic-fields\" class=\"example-frame\" title=\"Contact Dynamic Fields Demo\"></iframe>\n\n## Overview\n\nThis example shows how to dynamically show or hide form fields based on user selection. When the user chooses their preferred contact method, only the relevant input field is displayed.\n\n**Key patterns demonstrated:**\n\n- Field hidden when contact method doesn't match\n- Field becomes required when contact method matches\n- Clean user experience - only relevant fields shown\n\n## Implementation\n\n```typescript\nimport { Component, signal } from '@angular/core';\nimport { DynamicForm, type FormConfig } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-contact-form',\n imports: [DynamicForm],\n template: `<form [dynamic-form]=\"config\" [(value)]=\"formValue\"></form>`,\n})\nexport class ContactFormComponent {\n formValue = signal({});\n\n config = {\n fields: [\n {\n key: 'name',\n type: 'input',\n value: '',\n label: 'Full Name',\n required: true,\n },\n {\n key: 'contactMethod',\n type: 'select',\n value: '',\n label: 'Preferred Contact Method',\n required: true,\n options: [\n { value: 'email', label: 'Email' },\n { value: 'phone', label: 'Phone' },\n { value: 'mail', label: 'Postal Mail' },\n ],\n },\n {\n key: 'email',\n type: 'input',\n value: '',\n label: 'Email Address',\n email: true,\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'contactMethod',\n operator: 'notEquals',\n value: 'email',\n },\n },\n {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'contactMethod',\n operator: 'equals',\n value: 'email',\n },\n },\n ],\n },\n {\n key: 'phone',\n type: 'input',\n value: '',\n label: 'Phone Number',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'contactMethod',\n operator: 'notEquals',\n value: 'phone',\n },\n },\n {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'contactMethod',\n operator: 'equals',\n value: 'phone',\n },\n },\n ],\n props: { type: 'tel' },\n },\n {\n key: 'address',\n type: 'textarea',\n value: '',\n label: 'Mailing Address',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'contactMethod',\n operator: 'notEquals',\n value: 'mail',\n },\n },\n {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'contactMethod',\n operator: 'equals',\n value: 'mail',\n },\n },\n ],\n props: { rows: 3 },\n },\n {\n key: 'message',\n type: 'textarea',\n value: '',\n label: 'Message',\n required: true,\n minLength: 10,\n props: { rows: 4 },\n },\n ],\n } as const satisfies FormConfig;\n}\n```\n\n## How It Works\n\n### Conditional Visibility\n\nEach contact field uses the `logic` array to control visibility:\n\n```typescript\nlogic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'contactMethod',\n operator: 'notEquals',\n value: 'email',\n },\n },\n];\n```\n\nWhen `contactMethod !== 'email'`, the email field is hidden.\n\n### Conditional Required\n\nThe same `logic` array also makes fields required when visible:\n\n```typescript\nlogic: [\n {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'contactMethod',\n operator: 'equals',\n value: 'email',\n },\n },\n];\n```\n\nThis ensures that visible contact fields are required.\n\n## Use Cases\n\n- Contact forms with multiple contact options\n- Preference forms\n- Survey forms with branching logic\n- Customer support request forms\n\n## Related Documentation\n\n- **[Conditional Logic](../../dynamic-behavior/conditional-logic/overview/)** - Full conditional logic guide\n- **[Validation](../../validation/basics/)** - Form validation\n- **[Contact Form](../contact-form/)** - Basic contact form example"
105
- },
106
- {
107
- id: "contact-form",
108
- title: "Contact Form",
109
- category: "examples",
110
- content: "[\u2190 Back to Quick Start](/examples)\n\nSimple contact form demonstrating basic form fields, validation, and user input.\n\n## Live Demo\n\n<iframe src=\"http://localhost:4201/#/examples/contact\" class=\"example-frame\" title=\"Contact Form Demo\"></iframe>\n\n## Overview\n\nThis example shows a basic contact form with:\n\n- Text inputs (name, email, phone)\n- Textarea for messages\n- Date selection\n- Basic validation (required, email format)\n- Real-time validation feedback\n\n## Implementation\n\n```typescript\nimport { Component } from '@angular/core';\nimport { DynamicForm, FormConfig } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-contact-form',\n imports: [DynamicForm],\n template: `<form [dynamic-form]=\"config\"></form>`,\n})\nexport class ContactFormComponent {\n config = {\n fields: [\n {\n key: 'firstName',\n type: 'input',\n label: 'First Name',\n value: '',\n required: true,\n props: { placeholder: 'Your first name' },\n },\n {\n key: 'lastName',\n type: 'input',\n label: 'Last Name',\n value: '',\n required: true,\n props: { placeholder: 'Your last name' },\n },\n {\n key: 'email',\n type: 'input',\n label: 'Email',\n value: '',\n required: true,\n email: true,\n props: {\n type: 'email',\n placeholder: 'email@example.com',\n },\n },\n {\n key: 'phone',\n type: 'input',\n label: 'Phone',\n value: '',\n props: {\n type: 'tel',\n placeholder: '+1 (555) 000-0000',\n },\n },\n {\n key: 'subject',\n type: 'select',\n label: 'Subject',\n value: '',\n required: true,\n options: [\n { value: 'general', label: 'General Inquiry' },\n { value: 'support', label: 'Technical Support' },\n { value: 'sales', label: 'Sales Question' },\n { value: 'feedback', label: 'Feedback' },\n ],\n props: { placeholder: 'Select a subject' },\n },\n {\n key: 'message',\n type: 'textarea',\n label: 'Message',\n value: '',\n required: true,\n minLength: 10,\n maxLength: 500,\n validationMessages: {\n required: 'Please enter your message',\n minLength: 'Message must be at least 10 characters',\n maxLength: 'Message cannot exceed 500 characters',\n },\n props: {\n placeholder: 'Tell us how we can help...',\n rows: 5,\n },\n },\n {\n key: 'subscribe',\n type: 'checkbox',\n label: 'Subscribe to newsletter',\n value: false,\n },\n {\n type: 'submit',\n key: 'submit',\n label: 'Send Message',\n props: { color: 'primary' },\n },\n ],\n } as const satisfies FormConfig;\n}\n```\n\n## Key Features\n\n### Field Validation\n\nAll critical fields have validation:\n\n```typescript\n{\n key: 'email',\n required: true,\n email: true, // Built-in email validator\n}\n\n{\n key: 'message',\n required: true,\n minLength: 10,\n maxLength: 500,\n}\n```\n\n### User-Friendly Placeholders\n\nEvery field includes helpful placeholder text:\n\n```typescript\nprops: {\n placeholder: 'email@example.com', // Shows expected format\n}\n```\n\n### Subject Categorization\n\nDropdown allows users to categorize their inquiry:\n\n```typescript\n{\n key: 'subject',\n type: 'select',\n options: [\n { value: 'general', label: 'General Inquiry' },\n { value: 'support', label: 'Technical Support' },\n // ...\n ],\n}\n```\n\n## Use Cases\n\n- Customer support forms\n- Lead generation\n- General inquiries\n- Feedback collection\n- Event registration\n- Newsletter signup\n\n## Related Examples\n\n- **[User Registration](../user-registration/)** - Multi-step form with conditional fields\n- **[Login Form](../login-form/)** - Simple authentication\n\n## Related Documentation\n\n- **[Validation](../../validation/basics/)** - Form validation guide\n- **[Material Integration](../../ui-libs-integrations/material/)** - Material Design styling"
111
- },
112
- {
113
- id: "enterprise-features",
114
- title: "Enterprise Features",
115
- category: "examples",
116
- content: "[\u2190 Back to Quick Start](/examples)\n\nComplex configuration form demonstrating AND/OR logic for enterprise feature gating.\n\n## Live Demo\n\n<iframe src=\"http://localhost:4201/#/examples/enterprise-features\" class=\"example-frame\" title=\"Enterprise Features Demo\"></iframe>\n\n## Overview\n\nThis example shows how to use combined AND/OR conditions to control access to premium features based on account type and team size. Enterprise features are only shown when specific criteria are met.\n\n**Key patterns demonstrated:**\n\n- AND/OR logic for complex conditions\n- Multiple fields affecting each other\n- Enterprise feature gating\n\n## Implementation\n\n```typescript\nimport { Component, signal } from '@angular/core';\nimport { DynamicForm, type FormConfig } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-enterprise-features-form',\n imports: [DynamicForm],\n template: `<form [dynamic-form]=\"config\" [(value)]=\"formValue\"></form>`,\n})\nexport class EnterpriseFeaturesFormComponent {\n formValue = signal({});\n\n config = {\n fields: [\n {\n key: 'accountType',\n type: 'select',\n value: '',\n label: 'Account Type',\n required: true,\n options: [\n { value: 'free', label: 'Free' },\n { value: 'pro', label: 'Pro' },\n { value: 'enterprise', label: 'Enterprise' },\n ],\n },\n {\n key: 'teamSize',\n type: 'input',\n value: null,\n label: 'Team Size',\n min: 1,\n props: {\n type: 'number',\n hint: 'Number of team members',\n },\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'free',\n },\n },\n ],\n },\n {\n key: 'basicFeaturesTitle',\n type: 'text',\n label: 'Basic Features',\n props: { elementType: 'h4' },\n },\n {\n key: 'analyticsEnabled',\n type: 'toggle',\n value: true,\n label: 'Enable Analytics',\n },\n {\n key: 'notificationsEnabled',\n type: 'toggle',\n value: true,\n label: 'Enable Notifications',\n },\n {\n key: 'proFeaturesTitle',\n type: 'text',\n label: 'Pro Features',\n props: { elementType: 'h4' },\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'free',\n },\n },\n ],\n },\n {\n key: 'apiAccess',\n type: 'toggle',\n value: false,\n label: 'Enable API Access',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'free',\n },\n },\n ],\n },\n {\n key: 'advancedReports',\n type: 'toggle',\n value: false,\n label: 'Enable Advanced Reports',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'free',\n },\n },\n ],\n },\n {\n key: 'enterpriseFeaturesTitle',\n type: 'text',\n label: 'Enterprise Features',\n props: { elementType: 'h4' },\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'or',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'notEquals',\n value: 'enterprise',\n },\n {\n type: 'fieldValue',\n fieldPath: 'teamSize',\n operator: 'less',\n value: 10,\n },\n ],\n },\n },\n ],\n },\n {\n key: 'ssoEnabled',\n type: 'toggle',\n value: false,\n label: 'Enable SSO (Single Sign-On)',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'or',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'notEquals',\n value: 'enterprise',\n },\n {\n type: 'fieldValue',\n fieldPath: 'teamSize',\n operator: 'less',\n value: 10,\n },\n ],\n },\n },\n ],\n },\n {\n key: 'customBranding',\n type: 'toggle',\n value: false,\n label: 'Enable Custom Branding',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'or',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'free',\n },\n {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'pro',\n },\n ],\n },\n },\n ],\n },\n {\n key: 'dedicatedSupport',\n type: 'toggle',\n value: false,\n label: 'Enable Dedicated Support',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'or',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'notEquals',\n value: 'enterprise',\n },\n {\n type: 'fieldValue',\n fieldPath: 'teamSize',\n operator: 'less',\n value: 10,\n },\n ],\n },\n },\n ],\n },\n {\n type: 'submit',\n key: 'submit',\n label: 'Save Configuration',\n },\n ],\n } as const satisfies FormConfig;\n}\n```\n\n## How It Works\n\n### OR Conditions\n\nEnterprise features require enterprise account AND team size >= 10. They're hidden if EITHER condition fails:\n\n```typescript\n{\n key: 'ssoEnabled',\n type: 'toggle',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'or',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'notEquals',\n value: 'enterprise',\n },\n {\n type: 'fieldValue',\n fieldPath: 'teamSize',\n operator: 'less',\n value: 10,\n },\n ],\n },\n }],\n}\n```\n\nThis reads as: \"Hide SSO if account is NOT enterprise OR team size < 10\"\n\n### Tiered Feature Access\n\nDifferent tiers have different feature access:\n\n| Feature | Free | Pro | Enterprise (10+) |\n| ----------------- | ---- | --- | ---------------- |\n| Analytics | \u2713 | \u2713 | \u2713 |\n| Notifications | \u2713 | \u2713 | \u2713 |\n| API Access | \u2717 | \u2713 | \u2713 |\n| Advanced Reports | \u2717 | \u2713 | \u2713 |\n| SSO | \u2717 | \u2717 | \u2713 |\n| Custom Branding | \u2717 | \u2717 | \u2713 |\n| Dedicated Support | \u2717 | \u2717 | \u2713 |\n\n## Using AND Conditions\n\nFor features that require ALL conditions to be met, use `type: 'and'`:\n\n```typescript\n{\n type: 'hidden',\n condition: {\n type: 'and',\n conditions: [\n { type: 'fieldValue', fieldPath: 'accountType', operator: 'equals', value: 'enterprise' },\n { type: 'fieldValue', fieldPath: 'teamSize', operator: 'greaterOrEqual', value: 10 },\n { type: 'fieldValue', fieldPath: 'verified', operator: 'equals', value: true },\n ],\n },\n}\n```\n\n## Use Cases\n\n- SaaS feature configuration\n- Admin settings panels\n- Permission management\n- Subscription tier management\n\n## Related Documentation\n\n- **[Conditional Logic](../../dynamic-behavior/conditional-logic/overview/)** - Full conditional logic guide\n- **[Combining Conditions](../../dynamic-behavior/conditional-logic/overview/#combining-conditions)** - AND/OR logic\n- **[Form Groups](../../prebuilt/form-groups/)** - Organizing fields"
117
- },
118
- {
119
- id: "login-form",
120
- title: "Login Form",
121
- category: "examples",
122
- content: "[\u2190 Back to Quick Start](/examples)\n\nSimple login form demonstrating authentication UI with email and password fields.\n\n## Live Demo\n\n<iframe src=\"http://localhost:4201/#/examples/login\" class=\"example-frame\" title=\"Login Form Demo\"></iframe>\n\n## Overview\n\nA minimal login form showing:\n\n- Email input with validation\n- Password input (masked)\n- Remember me checkbox\n- Submit button\n- Clean, focused UI\n\n## Implementation\n\n```typescript\nimport { Component } from '@angular/core';\nimport { DynamicForm, FormConfig } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-login-form',\n imports: [DynamicForm],\n template: `<form [dynamic-form]=\"config\"></form>`,\n})\nexport class LoginFormComponent {\n config = {\n // Define common validation messages at the form level\n defaultValidationMessages: {\n required: 'This field is required',\n minLength: 'Must be at least {{requiredLength}} characters',\n },\n fields: [\n {\n key: 'title',\n type: 'text',\n label: 'Sign In',\n props: {\n elementType: 'h2',\n },\n },\n {\n key: 'email',\n type: 'input',\n label: 'Email Address',\n required: true,\n email: true,\n // Only specify custom message for 'email' - 'required' uses default\n validationMessages: {\n email: 'Please enter a valid email address',\n },\n props: {\n type: 'email',\n placeholder: 'your@email.com',\n hint: 'Enter the email associated with your account',\n },\n },\n {\n key: 'password',\n type: 'input',\n label: 'Password',\n required: true,\n minLength: 8,\n // Override default 'required' message with custom one\n validationMessages: {\n required: 'Password is required',\n },\n // 'minLength' will use default with interpolated {{requiredLength}}\n props: {\n type: 'password',\n placeholder: 'Enter your password',\n },\n },\n {\n key: 'remember',\n type: 'checkbox',\n label: 'Remember me for 30 days',\n },\n {\n type: 'submit',\n key: 'submit',\n label: 'Sign In',\n props: {\n color: 'primary',\n },\n },\n ],\n } as const satisfies FormConfig;\n}\n```\n\n## Key Features\n\n### Email Validation\n\nBuilt-in email format validation:\n\n```typescript\n{\n key: 'email',\n email: true, // Validates email format\n validationMessages: {\n email: 'Please enter a valid email',\n },\n}\n```\n\n### Password Security\n\nMinimum length requirement and masked input:\n\n```typescript\n{\n key: 'password',\n minLength: 8,\n props: {\n type: 'password', // Masks input\n },\n}\n```\n\n### Remember Me Option\n\nSimple checkbox for persistent login:\n\n```typescript\n{\n key: 'remember',\n type: 'checkbox',\n label: 'Remember me',\n}\n```\n\n## Common Enhancements\n\n### Add \"Forgot Password\" Link\n\n```typescript\n{\n type: 'text',\n key: 'forgotLink',\n label: '<a href=\"/forgot-password\">Forgot your password?</a>',\n}\n```\n\n### Social Login Buttons\n\nFor social login buttons, you can use text fields with links or handle this outside the form:\n\n```typescript\n{\n type: 'text',\n key: 'socialText',\n label: 'Or sign in with Google or GitHub',\n}\n```\n\n**Note:** Social login typically involves OAuth flows handled outside the form. Consider placing social login buttons in your component template rather than in the form configuration.\n\n### Add Sign Up Link\n\n```typescript\n{\n type: 'text',\n key: 'signupText',\n label: \"Don't have an account? <a href='/signup'>Sign up</a>\",\n}\n```\n\n## Use Cases\n\n- User authentication\n- Admin panels\n- Member portals\n- Dashboard access\n- Protected areas\n\n## Related Examples\n\n- **[User Registration](../user-registration/)** - Account creation\n- **[Contact Form](../contact-form/)** - Basic form with validation\n\n## Related Documentation\n\n- **[Validation](../../validation/basics/)** - Form validation guide\n- **[Material Integration](../../ui-libs-integrations/material/)** - Material Design styling"
123
- },
124
- {
125
- id: "paginated-form",
126
- title: "Paginated Form",
127
- category: "examples",
128
- content: "[\u2190 Back to Quick Start](/examples)\n\nA comprehensive multi-step registration form demonstrating the `page` field type for creating wizards and stepped workflows.\n\n## Live Demo\n\n<iframe src=\"http://localhost:4201/#/examples/paginated-form\" class=\"example-frame\" title=\"Paginated Form Demo\"></iframe>\n\n## Overview\n\nThis example showcases a 4-step registration form with:\n\n- **Page navigation** with Previous/Next/Submit buttons\n- **Per-page validation** that prevents moving forward with invalid data\n- **Progress tracking** through multiple steps\n- **Flattened form values** - page fields don't nest their children\n- **Mixed field types** across different steps\n\n## Implementation\n\n{% raw %}\n\n```typescript\nimport { Component, signal } from '@angular/core';\nimport { DynamicForm, FormConfig } from '@ng-forge/dynamic-forms';\nimport '@ng-forge/dynamic-forms-material';\n\n@Component({\n selector: 'app-paginated-form',\n imports: [DynamicForm, JsonPipe],\n template: `\n <form [dynamic-form]=\"config\" [(value)]=\"formValue\"></form>\n `,\n})\nexport class PaginatedFormComponent {\n formValue = signal({});\n\n config = {\n fields: [\n // Step 1: Personal Information\n {\n key: 'step1',\n type: 'page',\n fields: [\n {\n key: 'step1Title',\n type: 'text',\n label: 'Personal Information',\n props: { elementType: 'h2' },\n },\n {\n key: 'step1Description',\n type: 'text',\n label: 'Please provide your basic information',\n },\n {\n key: 'firstName',\n type: 'input',\n label: 'First Name',\n value: '',\n required: true,\n },\n {\n key: 'lastName',\n type: 'input',\n label: 'Last Name',\n value: '',\n required: true,\n },\n {\n key: 'birthDate',\n type: 'datepicker',\n label: 'Date of Birth',\n required: true,\n },\n {\n type: 'next',\n key: 'step1Next',\n label: 'Continue to Contact Info',\n },\n ],\n },\n\n // Step 2: Contact Information\n {\n key: 'step2',\n type: 'page',\n fields: [\n {\n key: 'step2Title',\n type: 'text',\n label: 'Contact Information',\n props: { elementType: 'h2' },\n },\n {\n key: 'step2Description',\n type: 'text',\n label: 'How can we reach you?',\n },\n {\n key: 'email',\n type: 'input',\n label: 'Email Address',\n value: '',\n required: true,\n email: true,\n },\n {\n key: 'phone',\n type: 'input',\n label: 'Phone Number',\n value: '',\n required: true,\n },\n {\n key: 'contactPreference',\n type: 'radio',\n label: 'Preferred Contact Method',\n value: 'email',\n options: [\n { value: 'email', label: 'Email' },\n { value: 'phone', label: 'Phone' },\n { value: 'both', label: 'Either' },\n ],\n },\n {\n type: 'row',\n key: 'step2Buttons',\n fields: [\n { type: 'previous', key: 'step2Previous', label: 'Back' },\n { type: 'next', key: 'step2Next', label: 'Continue' },\n ],\n },\n ],\n },\n\n // Step 3: Address\n {\n key: 'step3',\n type: 'page',\n fields: [\n {\n key: 'step3Title',\n type: 'text',\n label: 'Address',\n props: { elementType: 'h2' },\n },\n {\n key: 'step3Description',\n type: 'text',\n label: 'Where do you live?',\n },\n {\n key: 'street',\n type: 'input',\n label: 'Street Address',\n value: '',\n required: true,\n },\n {\n type: 'row',\n key: 'cityStateRow',\n fields: [\n { key: 'city', type: 'input', label: 'City', value: '', required: true, col: 6 },\n { key: 'state', type: 'select', label: 'State', required: true, options: [...], col: 6 },\n ],\n },\n {\n key: 'zipCode',\n type: 'input',\n label: 'ZIP Code',\n value: '',\n required: true,\n pattern: /^\\d{5}$/,\n },\n {\n type: 'row',\n key: 'step3Buttons',\n fields: [\n { type: 'previous', key: 'step3Previous', label: 'Back' },\n { type: 'next', key: 'step3Next', label: 'Continue' },\n ],\n },\n ],\n },\n\n // Step 4: Preferences & Completion\n {\n key: 'step4',\n type: 'page',\n fields: [\n {\n key: 'step4Title',\n type: 'text',\n label: 'Preferences',\n props: { elementType: 'h2' },\n },\n {\n key: 'step4Description',\n type: 'text',\n label: 'Tell us about your preferences',\n },\n {\n key: 'interests',\n type: 'multi-checkbox',\n label: 'Interests',\n options: [\n { value: 'technology', label: 'Technology' },\n { value: 'sports', label: 'Sports' },\n // ... more options\n ],\n },\n {\n key: 'newsletter',\n type: 'checkbox',\n label: 'Subscribe to newsletter',\n value: true,\n },\n {\n key: 'terms',\n type: 'checkbox',\n label: 'I agree to the terms and conditions',\n required: true,\n },\n {\n type: 'row',\n key: 'step4Buttons',\n fields: [\n { type: 'previous', key: 'step4Previous', label: 'Back' },\n { type: 'submit', key: 'submit', label: 'Complete Registration' },\n ],\n },\n ],\n },\n ],\n } as const satisfies FormConfig;\n}\n```\n\n{% endraw %}\n\n## Form Value Structure\n\nNote that page fields use `valueHandling: 'flatten'`, meaning their children are flattened to the parent level:\n\n```json\n{\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"birthDate\": \"1990-01-01\",\n \"email\": \"john@example.com\",\n \"phone\": \"+1-555-0000\",\n \"contactPreference\": \"email\",\n \"street\": \"123 Main St\",\n \"city\": \"New York\",\n \"state\": \"ny\",\n \"zipCode\": \"10001\",\n \"interests\": [\"technology\", \"sports\"],\n \"newsletter\": true,\n \"terms\": true\n}\n```\n\nThe page structure is **not reflected** in the form value - all fields are at the top level.\n\n## Key Features\n\n### Navigation Buttons\n\n- **`type: 'next'`** - Validates current page and moves to next step\n- **`type: 'previous'`** - Goes to previous step without validation\n- **`type: 'submit'`** - Validates entire form and submits\n\n### Per-Page Validation\n\nEach page validates independently. Users cannot proceed to the next page until all required fields on the current page are valid.\n\n### Page Configuration\n\n```typescript\n{\n key: 'step1', // Required key\n type: 'page', // Page field type\n fields: [ // Child fields\n // Add text fields for titles/descriptions if needed\n { key: 'title', type: 'text', label: 'Page Title', props: { elementType: 'h2' } },\n { key: 'desc', type: 'text', label: 'Page description...' },\n // ... other fields for this page\n ],\n}\n```\n\n## Performance & Lazy Loading\n\nDynamic Forms uses Angular's `@defer` blocks with **smart prefetching** to achieve true lazy loading while maintaining flicker-free navigation.\n\n### How It Works\n\n- **Current page loads immediately** - The active page renders instantly\n- **Adjacent pages (\xB11) prefetch** - Next/previous pages load in background for instant navigation\n- **Distant pages defer until idle** - Pages 2+ steps away load when browser is idle\n- **Automatic optimization** - No configuration needed - Dynamic Forms handles this automatically\n\n### Benefits\n\n```typescript\n// Example: Currently on step 2 of 5\nfields: [\n { key: 'step1', type: 'page', fields: [...] }, // \u2713 Prefetched (adjacent)\n { key: 'step2', type: 'page', fields: [...] }, // \u2713 Visible (current)\n { key: 'step3', type: 'page', fields: [...] }, // \u2713 Prefetched (adjacent)\n { key: 'step4', type: 'page', fields: [...] }, // \u23F3 Deferred (distant)\n { key: 'step5', type: 'page', fields: [...] }, // \u23F3 Deferred (distant)\n]\n```\n\n**Performance advantages:**\n\n- \u26A1 **Zero flicker navigation** - Adjacent pages prefetched for instant next/previous\n- \u{1F680} **Faster initial load** - Only 3 pages load immediately, distant pages defer until idle\n- \u23F1\uFE0F **Better Time to Interactive (TTI)** - Reduced initial JavaScript parsing/compilation\n- \u{1F4F1} **Mobile-friendly** - Lower startup cost on slower devices\n- \u{1F3AF} **Optimized user experience** - Smooth page transitions without loading states\n\n### Technical Details\n\nUnder the hood, the page orchestrator uses a **2-tier loading strategy**:\n\n```typescript\n@if (i === currentPageIndex || i === currentPageIndex + 1 || i === currentPageIndex - 1) {\n <!-- Current and adjacent pages: render immediately (but hide adjacent) -->\n @defer (on immediate) {\n <page-field [isVisible]=\"i === currentPageIndex\" />\n }\n} @else {\n <!-- Distant pages: defer until idle -->\n @defer (on idle) {\n <page-field [isVisible]=\"false\" />\n }\n}\n```\n\nThis means:\n\n- **Current + adjacent pages render immediately** - Using `@defer (on immediate)` to render during browser idle\n- **Visibility controlled via input and CSS** - Adjacent pages are fully rendered but hidden with `display: none`\n- **Initial load optimization** - Only 3 pages render initially, distant pages defer until idle\n- **Zero flicker navigation** - Next/previous pages already rendered, just toggle visibility\n- **Once loaded, pages persist** - Pages remain in DOM (hidden with CSS) after initial load\n\nThe primary benefit is **optimizing initial load performance**, not ongoing memory usage.\n\n### Best Practices\n\nFor optimal performance with multi-step forms:\n\n1. **Keep pages focused** - Limit each page to 5-10 fields for best UX\n2. **Put heavy pages later** - Place pages with expensive operations or large datasets later in the flow so they defer until idle\n3. **Front-load critical data** - Place important fields in early pages (they prefetch immediately)\n4. **Leverage the 3-page window** - Only current + adjacent pages load immediately, so structure your flow accordingly\n\n**Example optimization:**\n\n```typescript\nfields: [\n {\n key: 'basicInfo',\n type: 'page',\n fields: [\n /* lightweight fields */\n ],\n },\n {\n key: 'contact',\n type: 'page',\n fields: [\n /* lightweight fields */\n ],\n },\n {\n key: 'address',\n type: 'page',\n fields: [\n /* lightweight fields */\n ],\n },\n // These won't load until user reaches page 2-4 (when idle)\n {\n key: 'preferences',\n type: 'page',\n fields: [\n /* heavy multi-checkbox with 100 options */\n ],\n },\n {\n key: 'advanced',\n type: 'page',\n fields: [\n /* complex conditional logic */\n ],\n },\n];\n```\n\nWith this structure:\n\n- **Pages 1-2** load immediately (current + adjacent)\n- **Page 3** prefetches when you reach page 2\n- **Pages 4-5** defer until browser is idle, saving initial load time\n\n## Common Enhancements\n\n### Dynamic Steps\n\nShow/hide pages based on user choices:\n\n```typescript\n{\n key: 'businessInfo',\n type: 'page',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'notEquals',\n value: 'business',\n },\n }],\n fields: [/* ... */],\n}\n```\n\n### Progress Indicator\n\nAdd a custom progress component:\n\n{% raw %}\n\n```typescript\ntemplate: `\n <div class=\"progress-bar\">\n Step {{ currentPage() + 1 }} of {{ totalPages }}\n </div>\n <form [dynamic-form]=\"config\" [(value)]=\"formValue\" />\n`;\n```\n\n{% endraw %}\n\n### Conditional Validation\n\nApply different validation rules per step:\n\n```typescript\n{\n key: 'taxId',\n type: 'input',\n label: 'Tax ID',\n value: '',\n validators: [{\n type: 'required',\n when: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n }],\n}\n```\n\n## Use Cases\n\n- **Multi-step registration** - Break long forms into digestible steps\n- **Onboarding flows** - Guide users through setup processes\n- **Checkout processes** - Separate shipping, payment, and review\n- **Survey forms** - Organize questions into logical sections\n- **Complex data entry** - Reduce cognitive load with progressive disclosure\n\n## Related Examples\n\n- [User Registration Form](../user-registration) - Single-page registration with validation\n- [Contact Form](../contact-form) - Simple contact form\n- [Login Form](../login-form) - Basic authentication\n\n## Related Documentation\n\n- [Conditional Logic](../../dynamic-behavior/conditional-logic/overview) - Show/hide pages dynamically\n- [Validation](../../validation/basics) - Per-page and cross-page validation\n- [Material Integration](../../ui-libs-integrations/material/) - Material Design styling"
129
- },
130
- {
131
- id: "shipping-billing-address",
132
- title: "Shipping Billing Address",
133
- category: "examples",
134
- content: "[\u2190 Back to Quick Start](/examples)\n\nCheckout form demonstrating the common \"same as billing\" pattern for shipping addresses.\n\n## Live Demo\n\n<iframe src=\"http://localhost:4201/#/examples/shipping-billing-address\" class=\"example-frame\" title=\"Shipping Billing Address Demo\"></iframe>\n\n## Overview\n\nThis example shows how to toggle an entire group of fields based on a checkbox. When \"Shipping same as billing\" is checked, the shipping address fields are hidden.\n\n**Key patterns demonstrated:**\n\n- Entire group hidden/shown with single condition\n- Checkbox controls form complexity\n- Reduces user effort when addresses are the same\n\n## Implementation\n\n```typescript\nimport { Component, signal } from '@angular/core';\nimport { DynamicForm, type FormConfig } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-shipping-billing-form',\n imports: [DynamicForm],\n template: `<form [dynamic-form]=\"config\" [(value)]=\"formValue\"></form>`,\n})\nexport class ShippingBillingFormComponent {\n formValue = signal({});\n\n config = {\n fields: [\n {\n key: 'billingTitle',\n type: 'text',\n label: 'Billing Address',\n props: { elementType: 'h4' },\n },\n {\n key: 'billingAddress',\n type: 'group',\n fields: [\n {\n key: 'street',\n type: 'input',\n value: '',\n label: 'Street Address',\n required: true,\n },\n {\n key: 'city',\n type: 'input',\n value: '',\n label: 'City',\n required: true,\n },\n {\n key: 'state',\n type: 'input',\n value: '',\n label: 'State/Province',\n required: true,\n },\n {\n key: 'zipCode',\n type: 'input',\n value: '',\n label: 'ZIP/Postal Code',\n required: true,\n },\n ],\n },\n {\n key: 'sameAsBilling',\n type: 'checkbox',\n value: false,\n label: 'Shipping address is same as billing address',\n },\n {\n key: 'shippingTitle',\n type: 'text',\n label: 'Shipping Address',\n props: { elementType: 'h4' },\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'sameAsBilling',\n operator: 'equals',\n value: true,\n },\n },\n ],\n },\n {\n key: 'shippingAddress',\n type: 'group',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'sameAsBilling',\n operator: 'equals',\n value: true,\n },\n },\n ],\n fields: [\n {\n key: 'street',\n type: 'input',\n value: '',\n label: 'Street Address',\n required: true,\n },\n {\n key: 'city',\n type: 'input',\n value: '',\n label: 'City',\n required: true,\n },\n {\n key: 'state',\n type: 'input',\n value: '',\n label: 'State/Province',\n required: true,\n },\n {\n key: 'zipCode',\n type: 'input',\n value: '',\n label: 'ZIP/Postal Code',\n required: true,\n },\n ],\n },\n {\n type: 'submit',\n key: 'submit',\n label: 'Continue to Payment',\n },\n ],\n } as const satisfies FormConfig;\n}\n```\n\n## How It Works\n\n### Checkbox Toggle\n\nA simple checkbox controls whether shipping fields are visible:\n\n```typescript\n{\n key: 'sameAsBilling',\n type: 'checkbox',\n value: false,\n label: 'Shipping address is same as billing address',\n}\n```\n\n### Group Visibility\n\nThe entire shipping address group is hidden when the checkbox is checked:\n\n```typescript\n{\n key: 'shippingAddress',\n type: 'group',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'sameAsBilling',\n operator: 'equals',\n value: true,\n },\n }],\n fields: [\n // All shipping fields...\n ],\n}\n```\n\n### Section Titles\n\nThe section title is also hidden along with the group:\n\n```typescript\n{\n key: 'shippingTitle',\n type: 'text',\n label: 'Shipping Address',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'sameAsBilling',\n operator: 'equals',\n value: true,\n },\n }],\n}\n```\n\n## Use Cases\n\n- E-commerce checkout flows\n- Order forms\n- Account billing setup\n- Subscription management\n\n## Related Documentation\n\n- **[Conditional Logic](../../dynamic-behavior/conditional-logic/overview/)** - Full conditional logic guide\n- **[Form Groups](../../prebuilt/form-groups/)** - Working with groups\n- **[Checkbox Fields](../../schema-fields/field-types/)** - Field types reference"
135
- },
136
- {
137
- id: "simplified-array-form",
138
- title: "Simplified Array Form",
139
- category: "examples",
140
- content: "[\u2190 Back to Quick Start](/examples)\n\nDynamic arrays using the simplified API with auto-generated add/remove buttons, template-based item definitions, and both primitive and object arrays.\n\n## Live Demo\n\n<iframe src=\"http://localhost:4201/#/examples/simplified-array\" class=\"example-frame\" title=\"Simplified Array Demo\"></iframe>\n\n## Overview\n\nThis example showcases the simplified array API with:\n\n- **Primitive arrays** (tags) with flat scalar values\n- **Object arrays** (contacts) with multiple fields per item\n- **Empty arrays** that start with no items\n- **Button customization** including opt-out\n- **Auto-generated add/remove buttons**\n\n## Implementation\n\n```typescript\nimport { Component, signal } from '@angular/core';\nimport { DynamicForm, FormConfig } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-simplified-array-form',\n imports: [DynamicForm],\n template: `<form [dynamic-form]=\"config\" [(value)]=\"formValue\"></form>`,\n})\nexport class SimplifiedArrayFormComponent {\n formValue = signal({});\n\n config = {\n fields: [\n // Primitive array \u2014 tags\n {\n key: 'tags',\n type: 'array',\n template: {\n key: 'value',\n type: 'input',\n label: 'Tag',\n required: true,\n minLength: 2,\n props: { placeholder: 'Enter a tag' },\n },\n value: ['angular', 'typescript'],\n addButton: { label: 'Add Tag', props: { color: 'primary' } },\n removeButton: { label: 'Remove', props: { color: 'warn' } },\n },\n\n // Object array \u2014 contacts\n {\n key: 'contacts',\n type: 'array',\n template: [\n {\n key: 'name',\n type: 'input',\n label: 'Contact Name',\n required: true,\n minLength: 2,\n props: { placeholder: 'Enter contact name' },\n },\n {\n key: 'phone',\n type: 'input',\n label: 'Phone Number',\n props: { type: 'tel', placeholder: '5551234567' },\n },\n ],\n value: [\n { name: 'Jane Smith', phone: '5551234567' },\n { name: 'John Doe', phone: '5559876543' },\n ],\n addButton: { label: 'Add Contact', props: { color: 'primary' } },\n },\n\n // Empty array \u2014 notes\n {\n key: 'notes',\n type: 'array',\n template: {\n key: 'text',\n type: 'input',\n label: 'Note',\n props: { placeholder: 'Enter a note' },\n },\n addButton: { label: 'Add Note' },\n },\n\n // No remove button \u2014 categories\n {\n key: 'categories',\n type: 'array',\n template: { key: 'name', type: 'input', label: 'Category' },\n value: ['Frontend', 'Backend'],\n removeButton: false,\n addButton: { label: 'Add Category' },\n },\n\n { key: 'submit', type: 'submit', label: 'Save All', props: { color: 'primary' } },\n ],\n } as const satisfies FormConfig;\n}\n```\n\n## Key Features\n\n### Template-Based Items\n\nDefine the item structure once via `template`, and provide initial data via `value`:\n\n```typescript\n{\n key: 'tags',\n type: 'array',\n template: { key: 'value', type: 'input', label: 'Tag', required: true },\n value: ['angular', 'typescript'],\n}\n```\n\n### Auto-Generated Buttons\n\nAdd and remove buttons are generated automatically. Customize labels and props:\n\n```typescript\naddButton: { label: 'Add Tag', props: { color: 'primary' } },\nremoveButton: { label: 'Remove', props: { color: 'warn' } },\n```\n\n### Button Opt-Out\n\nDisable buttons entirely with `false`:\n\n```typescript\nremoveButton: false, // No remove buttons on items\naddButton: false, // No add button\n```\n\n## Use Cases\n\n- Tag lists and keyword management\n- Contact lists with multiple fields\n- Dynamic note-taking\n- Category management\n- Any repeating data entry\n\n## Related Examples\n\n- **[Array Form (Complete)](../array-form/)** - Full control with declarative buttons and EventBus\n- **[Paginated Form](../paginated-form/)** - Multi-step wizard form\n- **[Contact Form](../contact-form/)** - Basic contact form\n\n## Related Documentation\n\n- **[Simplified Array API](../../prebuilt/form-arrays/simplified/)** - Full API reference\n- **[Complete Array API](../../prebuilt/form-arrays/complete/)** - Advanced array features\n- **[Validation](../../validation/basics/)** - Form validation guide"
141
- },
142
- {
143
- id: "user-registration",
144
- title: "User Registration",
145
- category: "examples",
146
- content: "[\u2190 Back to Quick Start](/examples)\n\nComplete example of a user registration form with validation, conditional logic, and proper type safety.\n\n## Live Demo\n\n<iframe src=\"http://localhost:4201/#/examples/user-registration\" class=\"example-frame\" title=\"User Registration Demo\"></iframe>\n\n## Overview\n\nThis example demonstrates:\n\n- Multi-step form with validation\n- Password strength validation\n- Conditional fields (business account)\n- Terms and conditions acceptance\n- Type-safe form submission\n- Material Design integration\n\n## Complete Implementation\n\n```typescript\nimport { Component } from '@angular/core';\nimport { FormConfig, DynamicForm } from '@ng-forge/dynamic-forms';\n\nconst registrationConfig = {\n fields: [\n // Step 1: Account Information\n {\n type: 'page',\n key: 'accountPage',\n fields: [\n {\n type: 'text',\n key: 'accountText',\n label: 'Create your account',\n props: { elementType: 'h3' },\n },\n {\n key: 'username',\n type: 'input',\n value: '',\n label: 'Username',\n required: true,\n minLength: 3,\n maxLength: 20,\n pattern: '^[a-zA-Z0-9_]+$',\n validationMessages: {\n required: 'Username is required',\n minLength: 'Username must be at least 3 characters',\n maxLength: 'Username cannot exceed 20 characters',\n pattern: 'Username can only contain letters, numbers, and underscores',\n },\n props: {\n appearance: 'outline',\n hint: '3-20 characters, letters, numbers, and underscores only',\n },\n },\n {\n key: 'email',\n type: 'input',\n value: '',\n label: 'Email Address',\n required: true,\n email: true,\n validationMessages: {\n required: 'Email is required',\n email: 'Please enter a valid email address',\n },\n props: {\n type: 'email',\n appearance: 'outline',\n hint: \"We'll send a verification email\",\n },\n },\n {\n key: 'password',\n type: 'input',\n value: '',\n label: 'Password',\n required: true,\n minLength: 8,\n pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\\\d)(?=.*[@$!%*?&])[A-Za-z\\\\d@$!%*?&]+$',\n validationMessages: {\n required: 'Password is required',\n minLength: 'Password must be at least 8 characters',\n pattern: 'Password must include uppercase, lowercase, number, and special character',\n },\n props: {\n type: 'password',\n appearance: 'outline',\n hint: 'At least 8 characters with uppercase, lowercase, number, and special character',\n },\n },\n {\n key: 'confirmPassword',\n type: 'input',\n value: '',\n label: 'Confirm Password',\n required: true,\n validators: [\n {\n type: 'custom',\n expression: 'fieldValue === formValue.password',\n kind: 'passwordMismatch',\n },\n ],\n validationMessages: {\n required: 'Please confirm your password',\n passwordMismatch: 'Passwords do not match',\n },\n props: {\n type: 'password',\n appearance: 'outline',\n },\n },\n {\n type: 'next',\n key: 'nextToProfile',\n label: 'Continue to Profile',\n props: { color: 'primary' },\n },\n ],\n },\n\n // Step 2: Profile Information\n {\n type: 'page',\n key: 'profilePage',\n fields: [\n {\n type: 'row',\n key: 'nameRow',\n fields: [\n {\n key: 'firstName',\n type: 'input',\n value: '',\n label: 'First Name',\n required: true,\n col: 6,\n props: { appearance: 'outline' },\n },\n {\n key: 'lastName',\n type: 'input',\n value: '',\n label: 'Last Name',\n required: true,\n col: 6,\n props: { appearance: 'outline' },\n },\n ],\n },\n {\n key: 'dateOfBirth',\n type: 'datepicker',\n value: null,\n label: 'Date of Birth',\n required: true,\n maxDate: new Date(new Date().getFullYear() - 13, 0, 1),\n validationMessages: {\n required: 'Date of birth is required',\n maxDate: 'You must be at least 13 years old',\n },\n props: {\n appearance: 'outline',\n hint: 'You must be at least 13 years old',\n },\n },\n {\n key: 'accountType',\n type: 'radio',\n value: 'personal',\n label: 'Account Type',\n required: true,\n options: [\n { value: 'personal', label: 'Personal Account' },\n { value: 'business', label: 'Business Account' },\n ],\n props: { color: 'primary' },\n },\n {\n key: 'companyName',\n type: 'input',\n value: '',\n label: 'Company Name',\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'notEquals',\n value: 'business',\n },\n },\n {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n },\n ],\n props: { appearance: 'outline' },\n },\n {\n key: 'industry',\n type: 'select',\n value: '',\n label: 'Industry',\n options: [\n { value: 'tech', label: 'Technology' },\n { value: 'finance', label: 'Finance' },\n { value: 'healthcare', label: 'Healthcare' },\n { value: 'retail', label: 'Retail' },\n { value: 'education', label: 'Education' },\n { value: 'other', label: 'Other' },\n ],\n logic: [\n {\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'notEquals',\n value: 'business',\n },\n },\n {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n },\n ],\n props: {\n appearance: 'outline',\n placeholder: 'Select your industry',\n },\n },\n {\n type: 'row',\n key: 'navigationRow',\n fields: [\n {\n type: 'previous',\n key: 'backToAccount',\n label: 'Back',\n },\n {\n type: 'next',\n key: 'nextToPreferences',\n label: 'Continue',\n props: { color: 'primary' },\n },\n ],\n },\n ],\n },\n\n // Step 3: Preferences & Terms\n {\n type: 'page',\n key: 'preferencesPage',\n fields: [\n {\n key: 'interests',\n type: 'multi-checkbox',\n value: [],\n label: 'Interests',\n options: [\n { value: 'tech', label: 'Technology & Innovation' },\n { value: 'business', label: 'Business & Entrepreneurship' },\n { value: 'design', label: 'Design & Creativity' },\n { value: 'marketing', label: 'Marketing & Sales' },\n { value: 'development', label: 'Software Development' },\n ],\n props: { color: 'primary' },\n },\n {\n key: 'newsletter',\n type: 'checkbox',\n value: false,\n label: 'Subscribe to newsletter',\n props: { color: 'primary' },\n },\n {\n key: 'notifications',\n type: 'toggle',\n value: true,\n label: 'Enable email notifications',\n props: { color: 'primary' },\n },\n {\n key: 'terms',\n type: 'checkbox',\n value: false,\n label: 'I accept the terms and conditions',\n required: true,\n validationMessages: {\n required: 'You must accept the terms and conditions',\n },\n props: { color: 'primary' },\n },\n {\n key: 'privacy',\n type: 'checkbox',\n value: false,\n label: 'I have read and accept the privacy policy',\n required: true,\n validationMessages: {\n required: 'You must accept the privacy policy',\n },\n props: { color: 'primary' },\n },\n {\n type: 'row',\n key: 'finalNavigationRow',\n fields: [\n {\n type: 'previous',\n key: 'backToProfile',\n label: 'Back',\n },\n {\n type: 'submit',\n key: 'submitRegistration',\n label: 'Create Account',\n props: { color: 'primary' },\n },\n ],\n },\n ],\n },\n ],\n} as const satisfies FormConfig;\n\n@Component({\n selector: 'app-user-registration',\n imports: [DynamicForm],\n template: `<form [dynamic-form]=\"config\"></form>`,\n})\nexport class UserRegistrationComponent {\n config = registrationConfig;\n}\n```\n\n## Key Features\n\n### Multi-Step Form\n\nThe form uses page fields to create a wizard-style registration process:\n\n1. **Account Information** - Username, email, password\n2. **Profile Information** - Name, DOB, account type\n3. **Preferences & Terms** - Interests, notifications, agreements\n\nNavigation buttons (`next`, `previous`, `submit`) control flow between pages.\n\n### Password Validation\n\nStrong password requirements with pattern validation:\n\n```typescript\n{\n key: 'password',\n type: 'input',\n value: '',\n required: true,\n minLength: 8,\n pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\\\d)(?=.*[@$!%*?&])[A-Za-z\\\\d@$!%*?&]+$',\n // ...\n}\n```\n\nPassword confirmation with cross-field validation:\n\n```typescript\n{\n key: 'confirmPassword',\n validators: [{\n type: 'custom',\n expression: 'fieldValue === formValue.password',\n kind: 'passwordMismatch',\n }],\n validationMessages: {\n passwordMismatch: 'Passwords do not match',\n },\n}\n```\n\n### Conditional Business Fields\n\nCompany name and industry appear only for business accounts:\n\n```typescript\n{\n key: 'companyName',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'notEquals',\n value: 'business',\n },\n }, {\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n }],\n}\n```\n\n### Age Verification\n\nUses datepicker `maxDate` to ensure users are at least 13 years old:\n\n```typescript\n{\n key: 'dateOfBirth',\n type: 'datepicker',\n maxDate: new Date(new Date().getFullYear() - 13, 0, 1),\n validationMessages: {\n maxDate: 'You must be at least 13 years old',\n },\n}\n```\n\n### Type Safety\n\nFull type inference for form values:\n\n```typescript\ntype RegistrationValue = InferFormValue<typeof registrationConfig.fields>;\n\n// TypeScript knows the exact structure:\n// {\n// username: string;\n// email: string;\n// password: string;\n// confirmPassword: string;\n// firstName: string;\n// lastName: string;\n// dateOfBirth: Date | null;\n// accountType: string;\n// companyName?: string; // Conditional\n// industry?: string; // Conditional\n// interests?: string[];\n// newsletter?: boolean;\n// notifications?: boolean;\n// terms: boolean;\n// privacy: boolean;\n// }\n```\n\n## Variations\n\n### Single Page Registration\n\nFor simpler forms, remove page fields:\n\n```typescript\nconst simpleRegistrationConfig = {\n fields: [\n { key: 'email', type: 'input', value: '', required: true, email: true },\n { key: 'password', type: 'input', value: '', required: true, minLength: 8 },\n { key: 'terms', type: 'checkbox', value: false, required: true },\n { type: 'submit', key: 'submit', label: 'Sign Up' },\n ],\n} as const satisfies FormConfig;\n```\n\n### Social Login Integration\n\nAdd social login buttons before the form:\n\n```typescript\n{\n type: 'row',\n key: 'socialRow',\n fields: [\n { type: 'button', key: 'google', label: 'Sign up with Google', col: 6 },\n { type: 'button', key: 'github', label: 'Sign up with GitHub', col: 6 },\n ],\n}\n```\n\n## Related\n\n- **[Login Form](../login-form/)** - Simple authentication form\n- **[Paginated Form](../paginated-form/)** - Page field navigation patterns\n- **[Validation](../../validation/basics/)** - Validation guide\n- **[Conditional Logic](../../dynamic-behavior/conditional-logic/overview/)** - Dynamic field behavior\n- **[Material Integration](../../ui-libs-integrations/material/)** - Material Design styling"
147
- },
148
- {
149
- id: "value-derivation",
150
- title: "Value Derivation",
151
- category: "examples",
152
- content: "[\u2190 Back to Quick Start](/examples)\n\nThis example demonstrates automatic value derivation using expressions. Watch how changing input values automatically updates calculated fields.\n\n## Live Demo\n\n<iframe src=\"http://localhost:4201/#/examples/value-derivation\" class=\"example-frame\" title=\"Value Derivation Demo\"></iframe>\n\n## How It Works\n\n### Numeric Calculations\n\nThe order calculator shows chained derivations:\n\n1. **Subtotal** = Quantity \xD7 Unit Price\n2. **Tax** = Subtotal \xD7 Tax Rate / 100\n3. **Total** = Subtotal + Tax\n\nWhen you change any input, all dependent fields update automatically.\n\n### String Concatenation\n\nThe name fields demonstrate string derivation:\n\n- **Full Name** = First Name + \" \" + Last Name\n\n## Key Patterns\n\n### Using the `derivation` Shorthand\n\nThe simplest way to create a derived field is with the `derivation` property directly on the target field:\n\n```typescript\n{\n key: 'subtotal',\n type: 'input',\n disabled: true,\n derivation: 'formValue.quantity * formValue.unitPrice',\n}\n```\n\nThe expression is evaluated whenever its dependencies change. Dependencies are automatically detected from the expression.\n\n### Chained Derivations\n\nDerivations can reference other derived values. The system automatically processes them in the correct order:\n\n```typescript\n// subtotal depends on quantity and unitPrice\n{ key: 'subtotal', derivation: 'formValue.quantity * formValue.unitPrice' }\n\n// tax depends on subtotal (another derived field)\n{ key: 'tax', derivation: 'formValue.subtotal * formValue.taxRate / 100' }\n\n// total depends on both subtotal and tax\n{ key: 'total', derivation: 'formValue.subtotal + formValue.tax' }\n```\n\n### Derivation Flow\n\n```\nquantity \u2500\u2500\u2500\u2510\n \u251C\u2500\u2500 subtotal \u2500\u2500\u252C\u2500\u2500 tax \u2500\u2500\u252C\u2500\u2500 total\nunitPrice \u2500\u2500\u2518 \u2502 \u2502\n \u2502 \u2502\ntaxRate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n## Related\n\n- **[Value Derivation](../../dynamic-behavior/value-derivation/basics/)** - Core concepts, syntax, array derivations, debugging, and bidirectional patterns"
153
- },
154
- {
155
- id: "index",
156
- title: "Index",
157
- category: "installation",
158
- content: 'Get ng-forge dynamic forms up and running in your Angular project.\n\n## Requirements\n\n- **Angular 21+** - ng-forge dynamic forms requires Angular 21 or higher for signal forms support\n- **TypeScript 5.6+** - For best type inference results\n\n## Installation\n\nInstall the core library and your preferred UI integration:\n\n```bash group="install" name="npm"\nnpm install @ng-forge/dynamic-forms @ng-forge/dynamic-forms-material\n```\n\n```bash group="install" name="yarn"\nyarn add @ng-forge/dynamic-forms @ng-forge/dynamic-forms-material\n```\n\n```bash group="install" name="pnpm"\npnpm add @ng-forge/dynamic-forms @ng-forge/dynamic-forms-material\n```\n\n> This installs the core `@ng-forge/dynamic-forms` package and the Material Design integration. See [UI Framework Options](#ui-framework-options) below for other choices.\n\n## Configure Your App\n\nAdd the dynamic form provider to your app configuration:\n\n```typescript name="app.config.ts"\nimport { ApplicationConfig } from \'@angular/core\';\nimport { provideDynamicForm } from \'@ng-forge/dynamic-forms\';\nimport { withMaterialFields } from \'@ng-forge/dynamic-forms-material\';\n\nexport const appConfig: ApplicationConfig = {\n providers: [\n provideDynamicForm(...withMaterialFields()),\n // ... other providers\n ],\n};\n```\n\nThe `provideDynamicForm()` function registers field types and validators. The `withMaterialFields()` function provides all Material Design field components.\n\n## Create Your First Form\n\nCreate a simple login form to verify everything works:\n\n```typescript name="login.component.ts"\nimport { Component } from \'@angular/core\';\nimport { DynamicForm, type FormConfig } from \'@ng-forge/dynamic-forms\';\n\n@Component({\n selector: \'app-login\',\n imports: [DynamicForm],\n template: `<form [dynamic-form]="config"></form>`,\n})\nexport class LoginComponent {\n config = {\n fields: [\n {\n key: \'email\',\n type: \'input\',\n value: \'\',\n label: \'Email\',\n required: true,\n email: true,\n },\n {\n key: \'password\',\n type: \'input\',\n value: \'\',\n label: \'Password\',\n required: true,\n minLength: 8,\n props: { type: \'password\' },\n },\n {\n type: \'submit\',\n key: \'submit\',\n label: \'Sign In\',\n props: { color: \'primary\' },\n },\n ],\n } as const satisfies FormConfig;\n}\n```\n\nYou now have a working form with:\n\n- \u2705 Real-time validation with error messages\n- \u2705 TypeScript type inference\n- \u2705 Material Design styling\n- \u2705 Accessibility support\n- \u2705 Submit button auto-disables when invalid\n\n## UI Framework Options\n\nng-forge dynamic forms supports multiple UI frameworks. Install the integration package for your preferred framework:\n\n### Material Design (Preview)\n\n```bash group="install-material" name="npm"\nnpm install @ng-forge/dynamic-forms-material\n```\n\n```bash group="install-material" name="yarn"\nyarn add @ng-forge/dynamic-forms-material\n```\n\n```bash group="install-material" name="pnpm"\npnpm add @ng-forge/dynamic-forms-material\n```\n\n```typescript\nimport { withMaterialFields } from \'@ng-forge/dynamic-forms-material\';\n\nexport const appConfig: ApplicationConfig = {\n providers: [provideDynamicForm(...withMaterialFields())],\n};\n```\n\nSee [Material Integration](../ui-libs-integrations/material) for full documentation.\n\n### PrimeNG (Preview)\n\n```bash group="install-primeng" name="npm"\nnpm install @ng-forge/dynamic-forms-primeng\n```\n\n```bash group="install-primeng" name="yarn"\nyarn add @ng-forge/dynamic-forms-primeng\n```\n\n```bash group="install-primeng" name="pnpm"\npnpm add @ng-forge/dynamic-forms-primeng\n```\n\n```typescript\nimport { withPrimeNGFields } from \'@ng-forge/dynamic-forms-primeng\';\n\nexport const appConfig: ApplicationConfig = {\n providers: [provideDynamicForm(...withPrimeNGFields())],\n};\n```\n\nSee [PrimeNG Integration](../ui-libs-integrations/primeng) for full documentation.\n\n### Bootstrap (Preview)\n\n```bash group="install-bootstrap" name="npm"\nnpm install @ng-forge/dynamic-forms-bootstrap\n```\n\n```bash group="install-bootstrap" name="yarn"\nyarn add @ng-forge/dynamic-forms-bootstrap\n```\n\n```bash group="install-bootstrap" name="pnpm"\npnpm add @ng-forge/dynamic-forms-bootstrap\n```\n\n```typescript\nimport { withBootstrapFields } from \'@ng-forge/dynamic-forms-bootstrap\';\n\nexport const appConfig: ApplicationConfig = {\n providers: [provideDynamicForm(...withBootstrapFields())],\n};\n```\n\nSee [Bootstrap Integration](../ui-libs-integrations/bootstrap) for full documentation.\n\n### Ionic (Preview)\n\n```bash group="install-ionic" name="npm"\nnpm install @ng-forge/dynamic-forms-ionic\n```\n\n```bash group="install-ionic" name="yarn"\nyarn add @ng-forge/dynamic-forms-ionic\n```\n\n```bash group="install-ionic" name="pnpm"\npnpm add @ng-forge/dynamic-forms-ionic\n```\n\n```typescript\nimport { withIonicFields } from \'@ng-forge/dynamic-forms-ionic\';\n\nexport const appConfig: ApplicationConfig = {\n providers: [provideDynamicForm(...withIonicFields())],\n};\n```\n\nSee [Ionic Integration](../ui-libs-integrations/ionic) for full documentation.\n\n### Custom UI Components\n\nYou can also build your own field components using any UI library or custom styling:\n\n```typescript\nimport { MyCustomInputComponent } from \'./my-custom-input.component\';\n\nexport const appConfig: ApplicationConfig = {\n providers: [\n provideDynamicForm([\n { name: \'input\', loadComponent: () => MyCustomInputComponent },\n // ... more custom field types\n ]),\n ],\n};\n```\n\nSee [Custom Integration Guide](../advanced/custom-integrations) for building custom field components.\n\n## Next Steps\n\nNow that you have ng-forge dynamic forms installed, explore the core features:\n\n### Learn Core Concepts\n\n- **[Field Types](../schema-fields/field-types)** - Understand all available field types (input, select, checkbox, group, etc.)\n- **[Validation](../validation/basics)** - Add validation rules with shorthand syntax or conditional validators\n- **[Conditional Logic](../dynamic-behavior/conditional-logic/overview)** - Show/hide fields based on other field values\n- **[Type Safety](../advanced/type-safety/basics)** - TypeScript type inference for forms\n\n### Build Advanced Forms\n\n- **[Multi-Step Forms](../prebuilt/form-pages)** - Create wizard-style forms with page navigation\n- **[Repeatable Sections](../prebuilt/form-arrays/simplified)** - Dynamic form arrays for adding/removing fields\n- **[Conditional Validation](../validation/advanced#conditional-validators)** - Validators that activate based on conditions\n\n### Customize and Extend\n\n- **[i18n Setup](../dynamic-behavior/i18n)** - Add multi-language support to your forms\n- **[Events](../advanced/events)** - Handle custom form events\n- **[Custom Fields](../advanced/custom-integrations)** - Create your own field types\n\n## Get Help\n\n- \u{1F4AC} **[GitHub Discussions](https://github.com/ng-forge/ng-forge/discussions)** - Ask questions and get help\n- \u{1F41B} **[Issue Tracker](https://github.com/ng-forge/ng-forge/issues)** - Report bugs\n- \u{1F4D6} **[Documentation](../)** - Browse full documentation'
159
- },
160
- {
161
- id: "form-arrays-complete",
162
- title: "Form Arrays Complete",
163
- category: "prebuilt",
164
- content: "Arrays create dynamic collections of field values. Each item in the `fields` array defines **one array item** with its structure and initial values.\n\n> For most use cases, the **simplified array API** is recommended. See [Form Arrays (Simplified)](/prebuilt/form-arrays/simplified) for details. This page documents the complete API for advanced use cases.\n\n## Interactive Demo\n\n<iframe src=\"http://localhost:4201/#/examples/array\" class=\"example-frame\" title=\"Array Field Demo\"></iframe>\n\n## Structure Overview\n\nThe `fields` property supports two item formats:\n\n- **Single FieldDef** (not wrapped in array) \u2192 **Primitive item** - extracts field value directly\n- **Array of FieldDefs** \u2192 **Object item** - merges fields into an object\n\n```typescript\n// Primitive array: ['angular', 'typescript']\n{\n key: 'tags',\n type: 'array',\n fields: [\n { key: 'tag', type: 'input', value: 'angular' }, // Single field = primitive\n { key: 'tag', type: 'input', value: 'typescript' },\n ]\n}\n\n// Object array: [{ name: 'Alice', email: '...' }]\n{\n key: 'contacts',\n type: 'array',\n fields: [\n [ // Array of fields = object\n { key: 'name', type: 'input', label: 'Name', value: 'Alice' },\n { key: 'email', type: 'input', label: 'Email', value: 'alice@example.com' }\n ],\n ]\n}\n```\n\n## Empty Arrays (No Initial Items)\n\nFor arrays that start empty and are populated via buttons:\n\n```typescript\n{\n key: 'tags',\n type: 'array',\n fields: [] // No initial items - user adds via button\n}\n```\n\n## Array Size Validation\n\nUse `minLength` and `maxLength` on the array field to constrain the number of items. When violated, the form becomes invalid (e.g. submit button is disabled), but no field-level error message is shown.\n\n```typescript\n{\n key: 'tags',\n type: 'array',\n minLength: 1, // At least 1 item required\n maxLength: 5, // No more than 5 items\n fields: [\n [{ key: 'tag', type: 'input', label: 'Tag', value: 'default' }]\n ]\n}\n```\n\n| Property | Type | Description |\n| ----------- | -------- | ----------------------------- |\n| `minLength` | `number` | Minimum number of array items |\n| `maxLength` | `number` | Maximum number of array items |\n\nBoth properties are optional and can be used independently or together.\n\n## Initial Values\n\nInitial values are defined directly on each field via the `value` property - no separate `initialValue` is needed:\n\n```typescript\n{\n key: 'emails',\n type: 'array',\n fields: [\n // One initial item with pre-filled value (object item)\n [\n { key: 'email', type: 'input', label: 'Email', value: 'primary@example.com' }\n ]\n ]\n}\n```\n\n## Primitive Arrays (Simple Value Lists)\n\nFor simple arrays of primitive values like `['tag1', 'tag2']`, use a **single FieldDef per item** (not wrapped in an array):\n\n```typescript\n{\n key: 'tags',\n type: 'array',\n fields: [\n { key: 'tag', type: 'input', label: 'Tag', value: 'featured' },\n { key: 'tag', type: 'input', label: 'Tag', value: 'popular' }\n ]\n}\n```\n\nThis creates a true primitive array in the form value:\n\n```typescript\n{\n tags: ['featured', 'popular'];\n}\n```\n\nThe `key` property on each field is used for internal tracking but doesn't affect the output value.\n\n## Object Arrays (Multiple Fields per Item)\n\nFor arrays of objects with multiple fields, wrap the fields in an **inner array**:\n\n```typescript\n{\n key: 'contacts',\n type: 'array',\n fields: [\n [\n { key: 'name', type: 'input', label: 'Name', value: '' },\n { key: 'phone', type: 'input', label: 'Phone', value: '' }\n ]\n ]\n}\n```\n\nThis creates an array of objects:\n\n```typescript\n{\n contacts: [{ name: '', phone: '' }];\n}\n```\n\n## Nested Object Arrays (Using Groups)\n\nFor arrays with nested object structure, use a group field:\n\n```typescript\n{\n key: 'contacts',\n type: 'array',\n fields: [\n [\n {\n key: 'contact',\n type: 'group',\n fields: [\n { key: 'name', type: 'input', label: 'Name', value: '' },\n { key: 'phone', type: 'input', label: 'Phone', value: '' }\n ]\n }\n ]\n ]\n}\n```\n\nThis creates an array of nested objects (note the group key creates the nesting):\n\n```typescript\n{\n contacts: [{ contact: { name: '', phone: '' } }];\n}\n```\n\n## Heterogeneous Arrays (Mixed Primitives and Objects)\n\nArrays can contain both primitive and object items in the same array:\n\n```typescript\n{\n key: 'items',\n type: 'array',\n fields: [\n [{ key: 'label', type: 'input', value: 'Structured' }], // Object item (wrapped in array)\n { key: 'value', type: 'input', value: 'Simple' }, // Primitive item (single field)\n ]\n}\n```\n\nThis creates a heterogeneous array:\n\n```typescript\n{\n items: [{ label: 'Structured' }, 'Simple'];\n}\n```\n\n## Array vs Group\n\n- **Groups** create nested objects with keys: `{ address: { street: '', city: '' } }`\n- **Primitive Arrays** create lists of values: `{ tags: ['value1', 'value2'] }`\n- **Object Arrays** create lists of objects: `{ items: [{name: ''}, {name: ''}] }`\n\n## Dynamic Add/Remove\n\n### Declarative Approach (Recommended)\n\nUse button field types directly in your form configuration for declarative array manipulation:\n\n| Button Type | Placement | Description |\n| ------------------ | ---------------- | ------------------------------------------------ |\n| `addArrayItem` | Outside array | Appends a new item to the end of the array |\n| `prependArrayItem` | Outside array | Inserts a new item at the beginning of the array |\n| `insertArrayItem` | Outside array | Inserts a new item at a specific index |\n| `removeArrayItem` | Inside each item | Removes the current item from the array |\n| `popArrayItem` | Outside array | Removes the last item from the array |\n| `shiftArrayItem` | Outside array | Removes the first item from the array |\n\n**Important:** Add/prepend/insert buttons **require** a `template` property defining the new item structure. There is no fallback to the array's `fields[0]` - each button must explicitly define what structure to add.\n\nThe `template` can be:\n\n- **Single FieldDef** - creates a primitive item (field value is extracted directly)\n- **Array of FieldDefs** - creates an object item (fields merged into object)\n\n```typescript\n// Template for PRIMITIVE items (single field, not wrapped)\nconst tagTemplate = { key: 'tag', type: 'input', label: 'Tag' };\n\n// Template for OBJECT items (array of fields)\nconst contactTemplate = [\n { key: 'name', type: 'input', label: 'Name' },\n { key: 'phone', type: 'input', label: 'Phone' },\n // Remove button inside each item (no template needed)\n { key: 'remove', type: 'removeArrayItem', label: 'Remove' },\n];\n\n// Form configuration\n{\n fields: [\n {\n key: 'tags',\n type: 'array',\n fields: [], // Start empty - primitive array\n },\n // Add button for primitive items\n {\n key: 'addTag',\n type: 'addArrayItem',\n label: 'Add Tag',\n arrayKey: 'tags',\n template: tagTemplate, // Single field = primitive item\n },\n {\n key: 'contacts',\n type: 'array',\n fields: [], // Start empty - object array\n },\n // Add button for object items\n {\n key: 'addContact',\n type: 'addArrayItem',\n label: 'Add Contact',\n arrayKey: 'contacts',\n template: contactTemplate, // Array of fields = object item\n },\n ];\n}\n```\n\n### Programmatic Approach\n\nFor more control, use the event bus with the `arrayEvent` builder. **Note:** When using the programmatic approach, you must provide a template:\n\n```typescript\nimport { EventBus, arrayEvent } from '@ng-forge/dynamic-forms';\n\n// Inject the event bus\neventBus = inject(EventBus);\n\n// Template for PRIMITIVE items (single field, not wrapped)\ntagTemplate = { key: 'tag', type: 'input', label: 'Tag', value: '' };\n\n// Template for OBJECT items (array of fields)\ncontactTemplate = [\n { key: 'name', type: 'input', label: 'Name', value: '' },\n { key: 'phone', type: 'input', label: 'Phone', value: '' }\n];\n\n// Add PRIMITIVE item to end of array\naddTag() {\n this.eventBus.dispatch(arrayEvent('tags').append(this.tagTemplate));\n}\n\n// Add OBJECT item to end of array\naddContact() {\n this.eventBus.dispatch(arrayEvent('contacts').append(this.contactTemplate));\n}\n\n// Add item to beginning of array\nprependItem() {\n this.eventBus.dispatch(arrayEvent('contacts').prepend(this.contactTemplate));\n}\n\n// Add item at specific index\naddItemAt(index: number) {\n this.eventBus.dispatch(arrayEvent('contacts').insertAt(index, this.contactTemplate));\n}\n\n// Remove last item (no template needed)\nremoveLastItem() {\n this.eventBus.dispatch(arrayEvent('contacts').pop());\n}\n\n// Remove first item (no template needed)\nremoveFirstItem() {\n this.eventBus.dispatch(arrayEvent('contacts').shift());\n}\n\n// Remove item at specific index (no template needed)\nremoveItemAt(index: number) {\n this.eventBus.dispatch(arrayEvent('contacts').removeAt(index));\n}\n```\n\n## Use Cases\n\nArrays are ideal for:\n\n- Lists of simple values (tags, categories, keywords)\n- Repeating form sections (multiple addresses, phone numbers)\n- Dynamic collections where items can be added/removed\n- Collection-based data structures where order matters\n\n## Complete Example: Primitive Array with Initial Values\n\nHere's a complete working example of a primitive array field (simple value list) with initial values and declarative add/remove buttons:\n\n```typescript\nimport { FormConfig } from '@ng-forge/dynamic-forms';\n\n// Template for new tags - a row with an input and a remove button\nconst tagTemplate = {\n key: 'tag',\n type: 'row',\n fields: [\n {\n key: 'value',\n type: 'input',\n label: 'Tag',\n required: true,\n minLength: 2,\n },\n {\n key: 'removeTag',\n type: 'removeArrayItem',\n label: 'Remove',\n props: { color: 'warn' },\n },\n ],\n} as const;\n\nconst formConfig = {\n fields: [\n {\n key: 'tags',\n type: 'array',\n // Start with two initial tags using the same row structure\n fields: [\n [\n {\n key: 'tag',\n type: 'row',\n fields: [\n { key: 'value', type: 'input', label: 'Tag', value: 'featured', required: true, minLength: 2 },\n { key: 'removeTag', type: 'removeArrayItem', label: 'Remove', props: { color: 'warn' } },\n ],\n },\n ],\n [\n {\n key: 'tag',\n type: 'row',\n fields: [\n { key: 'value', type: 'input', label: 'Tag', value: 'popular', required: true, minLength: 2 },\n { key: 'removeTag', type: 'removeArrayItem', label: 'Remove', props: { color: 'warn' } },\n ],\n },\n ],\n ],\n },\n // Declarative add button - no EventBus needed\n {\n key: 'addTag',\n type: 'addArrayItem',\n label: 'Add Tag',\n arrayKey: 'tags',\n template: [tagTemplate],\n props: { color: 'primary' },\n },\n ],\n} as const satisfies FormConfig;\n\n// Form value: { tags: [{ tag: { value: 'featured' } }, { tag: { value: 'popular' } }] }\n```\n\n## Complete Example: Object Array with Initial Values\n\nHere's a complete working example of an object array field with initial values, validation, and declarative buttons:\n\n```typescript\nimport { FormConfig } from '@ng-forge/dynamic-forms';\n\n// Template for new contacts - includes a remove button inside each item\nconst contactTemplate = [\n { key: 'name', type: 'input', label: 'Contact Name', required: true, minLength: 2 },\n { key: 'phone', type: 'input', label: 'Phone Number', required: true, pattern: /^\\d{10}$/ },\n {\n key: 'relationship',\n type: 'select',\n label: 'Relationship',\n options: [\n { label: 'Family', value: 'family' },\n { label: 'Friend', value: 'friend' },\n { label: 'Colleague', value: 'colleague' },\n ],\n },\n {\n key: 'removeContact',\n type: 'removeArrayItem',\n label: 'Remove Contact',\n props: { color: 'warn' },\n },\n] as const;\n\nconst formConfig = {\n fields: [\n {\n key: 'contacts',\n type: 'array',\n // Start with one pre-filled contact (includes remove button)\n fields: [\n [\n { key: 'name', type: 'input', label: 'Contact Name', value: 'John Doe', required: true, minLength: 2 },\n {\n key: 'phone',\n type: 'input',\n label: 'Phone Number',\n value: '5551234567',\n required: true,\n pattern: /^\\d{10}$/,\n },\n {\n key: 'relationship',\n type: 'select',\n label: 'Relationship',\n value: 'family',\n options: [\n { label: 'Family', value: 'family' },\n { label: 'Friend', value: 'friend' },\n { label: 'Colleague', value: 'colleague' },\n ],\n },\n {\n key: 'removeContact',\n type: 'removeArrayItem',\n label: 'Remove Contact',\n props: { color: 'warn' },\n },\n ],\n ],\n },\n // Declarative add button - no EventBus needed\n {\n key: 'addContact',\n type: 'addArrayItem',\n label: 'Add Contact',\n arrayKey: 'contacts',\n template: contactTemplate,\n props: { color: 'primary' },\n },\n ],\n} as const satisfies FormConfig;\n\n// Form value: { contacts: [{ name: 'John Doe', phone: '5551234567', relationship: 'family' }] }\n```\n\n## Template Requirement\n\n**Important:** When adding items dynamically (via buttons or event bus), you must always provide an explicit `template`. There is no automatic fallback to use the first item's structure.\n\nThis design ensures:\n\n- Clear intent - each add operation explicitly defines what to add\n- Flexibility - different buttons can add different item structures\n- No ambiguity - the array's `fields` only defines initial items, not a \"default template\"\n\n## Heterogeneous Items\n\nArrays can have items with different field structures:\n\n```typescript\n{\n key: 'entries',\n type: 'array',\n fields: [\n // Item 0: Simple tag\n [{ key: 'tag', type: 'input', label: 'Tag', value: 'simple' }],\n // Item 1: Contact with more fields\n [\n { key: 'name', type: 'input', label: 'Name', value: 'Alice' },\n { key: 'email', type: 'input', label: 'Email', value: 'alice@example.com' }\n ]\n ]\n}\n```\n\nThis flexibility allows each item to have its own structure, though typically all items share the same structure for consistency.\n\n## Nesting Constraints\n\nArray fields can be used within:\n\n- Pages (top-level container)\n- Rows (for horizontal layouts)\n- Groups (for nested arrays within objects)\n\nArrays **cannot** contain:\n\n- Other array fields (no nested arrays)\n- Page fields\n\n## Allowed Children\n\nArrays can contain these field types:\n\n- Leaf fields (input, select, checkbox, etc.) -> creates flat or object arrays\n- Group fields with key -> creates nested object arrays `[{groupKey: {...}}, ...]`\n- Row fields -> creates flat object arrays `[{...}, {...}]` (rows don't add nesting)\n- Button fields (for remove operations inside each item)\n\nSee [Type Safety & Inference](../advanced/type-safety/basics) for details on how arrays affect type inference.\n\n## Conditional Visibility\n\nArray containers support the `logic` property to conditionally show or hide the entire array section based on form state.\n\n```typescript\n{\n key: 'dependents',\n type: 'array',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'hasDependents',\n operator: 'equals',\n value: false,\n },\n }],\n fields: [/* ... */],\n}\n```\n\nWhen the array is hidden, all items and add/remove buttons are hidden with it. Only `'hidden'` is supported as a logic type on containers.\n\nFor all available condition types and operators, see [Conditional Logic](../../../dynamic-behavior/conditional-logic/overview/).\n\n## When to Use the Complete API\n\nThe complete API documented on this page is the right choice when you need capabilities beyond what the simplified API offers:\n\n- **Heterogeneous items** -- Different field structures per item (e.g. some items have 2 fields, others have 5)\n- **Custom button placement** -- Positioning add/remove/prepend/insert buttons in specific locations relative to the array\n- **Programmatic control via EventBus** -- Dispatching `arrayEvent` commands for append, prepend, insertAt, pop, shift, and removeAt operations from component code\n- **Custom templates per button action** -- Different add buttons that each insert a different item structure (e.g. an \"Add Simple\" and an \"Add Detailed\" button)\n- **Mixed primitive and object items** -- Arrays containing both primitive values and object values in the same collection\n\nFor all other scenarios, the [simplified array API](/prebuilt/form-arrays/simplified) provides a more concise configuration with auto-generated buttons."
165
- },
166
- {
167
- id: "form-arrays-simplified",
168
- title: "Form Arrays Simplified",
169
- category: "prebuilt",
170
- content: "The simplified array API provides a concise way to define dynamic arrays. Instead of manually specifying each item in `fields`, you provide a `template` for the item structure and a `value` array with initial data. Add and remove buttons are generated automatically.\n\n> **When should I use this?** For most array use cases -- primitive lists, object lists, and standard add/remove workflows -- the simplified API is the right choice. For advanced scenarios like heterogeneous items or custom button placement, see [Form Arrays (Complete)](/prebuilt/form-arrays/complete).\n\n## Interactive Demo\n\n<iframe src=\"http://localhost:4201/#/examples/simplified-array\" class=\"example-frame\" title=\"Simplified Array Demo\"></iframe>\n\n## Overview\n\nThe simplified API uses two key properties:\n\n| Property | Description |\n| ----------- | -------------------------------------------------------------------------- |\n| `template` | Defines the structure of a single array item (single field or field array) |\n| `value` | Initial data array -- each element creates one pre-filled item |\n| `minLength` | Minimum number of array items (form invalid if fewer) |\n| `maxLength` | Maximum number of array items (form invalid if more) |\n\nThe `template` shape determines the array variant:\n\n- **Single field** (`template: { ... }`) -- primitive array (e.g. `['tag1', 'tag2']`)\n- **Array of fields** (`template: [{ ... }, { ... }]`) -- object array (e.g. `[{ name: 'Jane', phone: '555' }]`)\n\n```typescript\n// Discriminant: template is a single field \u2192 primitive array\n{ key: 'tags', type: 'array', template: { key: 'value', type: 'input', label: 'Tag' } }\n\n// Discriminant: template is an array of fields \u2192 object array\n{ key: 'contacts', type: 'array', template: [\n { key: 'name', type: 'input', label: 'Name' },\n { key: 'phone', type: 'input', label: 'Phone' },\n] }\n```\n\n## Primitive Arrays\n\nFor simple arrays of scalar values like `['angular', 'typescript']`, provide a single field as the `template` and a flat array as the `value`:\n\n```typescript\n{\n key: 'tags',\n type: 'array',\n template: { key: 'value', type: 'input', label: 'Tag', required: true },\n value: ['angular', 'typescript'],\n}\n```\n\nThis renders two input fields pre-filled with `'angular'` and `'typescript'`, each with an auto-generated **Remove** button. An **Add** button is placed after the array.\n\nForm value output:\n\n```typescript\n{\n tags: ['angular', 'typescript'];\n}\n```\n\n## Object Arrays\n\nFor arrays of objects like `[{ name: 'Jane', phone: '555' }]`, provide an array of fields as the `template` and an array of objects as the `value`:\n\n```typescript\n{\n key: 'contacts',\n type: 'array',\n template: [\n { key: 'name', type: 'input', label: 'Contact Name', required: true },\n { key: 'phone', type: 'input', label: 'Phone Number' },\n ],\n value: [\n { name: 'Jane', phone: '555-1234' },\n { name: 'Bob', phone: '555-5678' },\n ],\n}\n```\n\nEach value object is matched to the template by key. If a value object omits a key, the corresponding field renders without an initial value.\n\nForm value output:\n\n```typescript\n{\n contacts: [\n { name: 'Jane', phone: '555-1234' },\n { name: 'Bob', phone: '555-5678' },\n ];\n}\n```\n\n## Empty Arrays\n\nOmit `value` or set `value: []` to start with an empty array. Users populate it via the auto-generated **Add** button:\n\n```typescript\n// No value property \u2014 starts empty\n{\n key: 'tags',\n type: 'array',\n template: { key: 'value', type: 'input', label: 'Tag' },\n}\n\n// Explicit empty array \u2014 same behavior\n{\n key: 'emails',\n type: 'array',\n template: [\n { key: 'address', type: 'input', label: 'Email Address', required: true },\n { key: 'label', type: 'select', label: 'Type', options: [\n { label: 'Work', value: 'work' },\n { label: 'Personal', value: 'personal' },\n ] },\n ],\n value: [],\n}\n```\n\n## Button Customization\n\nBy default, the library generates an **Add** button (label: `\"Add\"`) after the array and a **Remove** button (label: `\"Remove\"`) inside each item. You can customize both via `addButton` and `removeButton`:\n\n```typescript\n{\n key: 'tags',\n type: 'array',\n template: { key: 'value', type: 'input', label: 'Tag' },\n value: ['angular'],\n addButton: { label: 'Add Tag', props: { color: 'primary' } },\n removeButton: { label: 'Delete', props: { color: 'warn' } },\n}\n```\n\n### Button Config Properties\n\n| Property | Type | Description |\n| -------- | ------------------------- | ---------------------------------------------------- |\n| `label` | `string` | Custom label text for the button |\n| `props` | `Record<string, unknown>` | Additional properties passed to the button component |\n\n## Button Opt-Out\n\nSet `addButton: false` or `removeButton: false` to suppress the corresponding auto-generated button entirely:\n\n```typescript\n// No add button \u2014 items are only removable, not addable\n{\n key: 'defaultTags',\n type: 'array',\n template: { key: 'value', type: 'input', label: 'Tag' },\n value: ['featured', 'popular'],\n addButton: false,\n}\n\n// No remove button \u2014 items are only addable, not removable\n{\n key: 'skills',\n type: 'array',\n template: { key: 'value', type: 'input', label: 'Skill' },\n value: ['TypeScript'],\n removeButton: false,\n}\n\n// No buttons at all \u2014 static list rendered from initial values\n{\n key: 'readonlyItems',\n type: 'array',\n template: { key: 'value', type: 'input', label: 'Item' },\n value: ['A', 'B', 'C'],\n addButton: false,\n removeButton: false,\n}\n```\n\nWhen `removeButton: false` is set for a primitive array, each item renders as a plain field without being wrapped in a row. When set for an object array, the remove button is simply omitted from the item's field list.\n\n## Complete Example: Primitive Array\n\nA full working configuration for a tag list with custom buttons:\n\n```typescript\nimport { FormConfig } from '@ng-forge/dynamic-forms';\n\nconst formConfig: FormConfig = {\n fields: [\n {\n key: 'projectName',\n type: 'input',\n label: 'Project Name',\n required: true,\n },\n {\n key: 'tags',\n type: 'array',\n template: { key: 'value', type: 'input', label: 'Tag', required: true, minLength: 2 },\n value: ['angular', 'open-source'],\n addButton: { label: 'Add Tag' },\n removeButton: { label: 'Remove Tag' },\n },\n ],\n};\n\n// Form value:\n// {\n// projectName: 'My Project',\n// tags: ['angular', 'open-source']\n// }\n```\n\n## Complete Example: Object Array\n\nA full working configuration for a contact list:\n\n```typescript\nimport { FormConfig } from '@ng-forge/dynamic-forms';\n\nconst formConfig: FormConfig = {\n fields: [\n {\n key: 'teamName',\n type: 'input',\n label: 'Team Name',\n required: true,\n },\n {\n key: 'members',\n type: 'array',\n template: [\n { key: 'name', type: 'input', label: 'Full Name', required: true },\n { key: 'email', type: 'input', label: 'Email', required: true },\n {\n key: 'role',\n type: 'select',\n label: 'Role',\n options: [\n { label: 'Developer', value: 'dev' },\n { label: 'Designer', value: 'design' },\n { label: 'Manager', value: 'manager' },\n ],\n },\n ],\n value: [\n { name: 'Alice', email: 'alice@example.com', role: 'dev' },\n { name: 'Bob', email: 'bob@example.com', role: 'design' },\n ],\n addButton: { label: 'Add Member' },\n removeButton: { label: 'Remove' },\n },\n ],\n};\n\n// Form value:\n// {\n// teamName: 'Frontend Team',\n// members: [\n// { name: 'Alice', email: 'alice@example.com', role: 'dev' },\n// { name: 'Bob', email: 'bob@example.com', role: 'design' },\n// ]\n// }\n```\n\n## Form Value Output\n\nThe simplified API produces the same form values as the complete API:\n\n| Template Shape | Value Shape | Output |\n| --------------- | --------------------- | ------------------------------------------------ |\n| Single field | Flat array of scalars | `{ tags: ['angular', 'typescript'] }` |\n| Array of fields | Array of objects | `{ contacts: [{ name: 'Jane', phone: '555' }] }` |\n| Any template | `[]` or omitted | `{ items: [] }` |\n\n## Array Size Validation\n\nUse `minLength` and `maxLength` to constrain the number of items. When violated, the form becomes invalid (e.g. submit button is disabled).\n\n```typescript\n{\n key: 'tags',\n type: 'array',\n template: { key: 'value', type: 'input', label: 'Tag' },\n value: ['angular'],\n minLength: 1,\n maxLength: 5,\n}\n```\n\nBoth properties are optional and can be used independently or together.\n\n## Conditional Visibility\n\nSimplified arrays support `logic` for conditional visibility, just like the complete API:\n\n```typescript\n{\n key: 'extras',\n type: 'array',\n template: { key: 'value', type: 'input', label: 'Extra' },\n value: [],\n logic: [{ type: 'hidden', condition: { type: 'fieldValue', fieldPath: 'showExtras', operator: 'equals', value: false } }],\n}\n```\n\n## Decision Guide: Simplified vs Complete API\n\n| Scenario | Recommended API |\n| ----------------------------------------------- | --------------- |\n| Uniform items with standard add/remove | **Simplified** |\n| Pre-filled values from a data source | **Simplified** |\n| Custom button labels or styling | **Simplified** |\n| Empty array populated by users | **Simplified** |\n| Static list (no add/remove buttons) | **Simplified** |\n| Heterogeneous items (different fields per item) | Complete |\n| Custom button placement (buttons outside array) | Complete |\n| Programmatic control via EventBus | Complete |\n| Multiple add buttons (append, prepend, insert) | Complete |\n| Nested arrays inside item templates | Complete |\n\nAs a rule of thumb: **start with the simplified API**. If you hit a limitation, switch to the [complete API](/prebuilt/form-arrays/complete)."
171
- },
172
- {
173
- id: "form-groups",
174
- title: "Form Groups",
175
- category: "prebuilt",
176
- content: "Groups nest form fields under a single key in the form value. This creates logical grouping for form data, not visual grouping.\n\n## Interactive Demo\n\n<iframe src=\"http://localhost:4201/#/examples/group\" class=\"example-frame\" title=\"Group Field Demo\"></iframe>\n\n## Basic Group\n\n```typescript\n{\n type: 'group',\n key: 'address',\n fields: [\n { key: 'street', type: 'input', label: 'Street', value: '' },\n { key: 'city', type: 'input', label: 'City', value: '' },\n { key: 'zip', type: 'input', label: 'ZIP', value: '' },\n ],\n}\n```\n\nThis creates a nested structure in the form value:\n\n```typescript\n{\n address: {\n street: '',\n city: '',\n zip: ''\n }\n}\n```\n\nGroups are for organizing form **data**, not UI. The visual presentation depends on your UI integration (Material, Bootstrap, etc.).\n\n## Complete Example\n\nHere's a complete working example of a group field with validation:\n\n```typescript\nimport { Component } from '@angular/core';\nimport { DynamicForm } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-user-profile-form',\n imports: [DynamicForm],\n template: `<form [dynamic-form]=\"formConfig\"></form>`,\n})\nexport class UserProfileFormComponent {\n formConfig = {\n fields: [\n {\n key: 'name',\n type: 'input',\n label: 'Full Name',\n value: '',\n required: true,\n },\n {\n key: 'address',\n type: 'group',\n fields: [\n {\n key: 'street',\n type: 'input',\n label: 'Street Address',\n value: '',\n required: true,\n },\n {\n key: 'city',\n type: 'input',\n label: 'City',\n value: '',\n required: true,\n },\n {\n key: 'state',\n type: 'input',\n label: 'State',\n value: '',\n required: true,\n maxLength: 2,\n },\n {\n key: 'zip',\n type: 'input',\n label: 'ZIP Code',\n value: '',\n required: true,\n pattern: /^\\d{5}$/,\n },\n ],\n },\n ],\n };\n}\n```\n\nThis produces a form value with nested structure:\n\n```typescript\n{\n name: 'John Doe',\n address: {\n street: '123 Main St',\n city: 'Springfield',\n state: 'IL',\n zip: '62701'\n }\n}\n```\n\n## Nesting Restrictions\n\nGroup fields can be used within:\n\n- Pages (top-level container)\n- Rows (for horizontal layouts)\n- Array fields (for creating object arrays where each array item is an object)\n\nGroups **cannot** be nested inside:\n\n- Other group fields (no nested groups)\n\n## Allowed Children\n\nGroups can contain:\n\n- Leaf fields (input, select, checkbox, etc.)\n- Row fields (for horizontal layouts within the group)\n\nSee [Type Safety & Inference](../advanced/type-safety/basics) for details on how groups affect type inference.\n\n## Conditional Visibility\n\nGroup containers support the `logic` property to conditionally show or hide the entire group (and all its nested fields) based on form state.\n\n```typescript\n{\n key: 'addressGroup',\n type: 'group',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'sameAsBilling',\n operator: 'equals',\n value: true,\n },\n }],\n fields: [\n { key: 'street', type: 'input', label: 'Street', value: '' },\n { key: 'city', type: 'input', label: 'City', value: '' },\n { key: 'zip', type: 'input', label: 'ZIP', value: '' },\n ],\n}\n```\n\nWhen the group is hidden, all its nested fields are hidden with it. Only `'hidden'` is supported as a logic type on containers (not `required`, `readonly`, or `disabled`).\n\nFor all available condition types and operators, see [Conditional Logic](../../dynamic-behavior/conditional-logic/overview/)."
177
- },
178
- {
179
- id: "form-pages",
180
- title: "Form Pages",
181
- category: "prebuilt",
182
- content: "Create multi-step forms by using page fields. When your form contains page fields, it automatically enters \"paged mode\" and renders with navigation controls.\n\n## Basic Multi-Step Form\n\nCreate a multi-step form by adding multiple page fields to your form configuration:\n\n```typescript\n{\n fields: [\n {\n key: 'account',\n type: 'page',\n fields: [\n { key: 'accountTitle', type: 'text', label: 'Account Information', props: { elementType: 'h3' } },\n { key: 'username', type: 'input', label: 'Username', value: '', required: true },\n { key: 'password', type: 'input', label: 'Password', value: '', props: { type: 'password' }, required: true },\n ],\n },\n {\n key: 'profile',\n type: 'page',\n fields: [\n { key: 'profileTitle', type: 'text', label: 'Profile Details', props: { elementType: 'h3' } },\n { key: 'firstName', type: 'input', label: 'First Name', value: '' },\n { key: 'lastName', type: 'input', label: 'Last Name', value: '' },\n ],\n },\n ],\n}\n```\n\n## Page Properties\n\nEach page field supports:\n\n- `key` (required) - Unique identifier for the page\n- `type: 'page'` (required) - Field type identifier\n- `fields` (required) - Array of child fields to render on this page\n\n## Page with Description\n\n```typescript\n{\n key: 'preferences',\n type: 'page',\n fields: [\n { key: 'newsletter', type: 'checkbox', label: 'Subscribe to newsletter', value: false },\n { key: 'notifications', type: 'checkbox', label: 'Enable notifications', value: false },\n ],\n}\n```\n\n## Paged Mode Behavior\n\nWhen your form contains page fields:\n\n- **Automatic Detection**: Form automatically enters \"paged mode\"\n- **Navigation Controls**: Previous/Next buttons are rendered automatically\n- **Validation**: Users must complete required fields before advancing to the next page\n- **Single Page View**: Only one page is visible at a time\n\n## Performance & Lazy Loading\n\nng-forge uses Angular's `@defer` blocks with smart prefetching to optimize page rendering while maintaining flicker-free navigation.\n\n### How It Works\n\nThe page orchestrator uses a **2-tier loading strategy**:\n\n**Tier 1: Current + Adjacent Pages (\xB11)**\n\n- Render immediately using `@defer (on immediate)`\n- Initially, only 3 pages load (current + 2 adjacent)\n- Adjacent pages are fully rendered but hidden with `display: none`\n- Ensures zero flicker when navigating forward/backward\n\n**Tier 2: Distant Pages (2+ steps away)**\n\n- Defer loading until browser is idle using `@defer (on idle)`\n- Lazy loading optimizes initial page load\n- Load automatically during browser idle time\n- Once loaded, pages remain in DOM (hidden with CSS)\n\n### Benefits\n\n```typescript\n// Example: User is on step 2 of 5\nfields: [\n { key: 'step1', type: 'page', ... }, // \u2713 Rendered (adjacent)\n { key: 'step2', type: 'page', ... }, // \u2713 Visible (current)\n { key: 'step3', type: 'page', ... }, // \u2713 Rendered (adjacent)\n { key: 'step4', type: 'page', ... }, // \u23F3 Deferred (distant)\n { key: 'step5', type: 'page', ... }, // \u23F3 Deferred (distant)\n]\n```\n\n**Performance advantages:**\n\n- \u26A1 **Zero navigation flicker** - Adjacent pages already rendered\n- \u{1F680} **Faster initial load** - Only 3 pages render immediately, distant pages defer until idle\n- \u23F1\uFE0F **Better Time to Interactive (TTI)** - Reduced initial JavaScript parsing/compilation\n- \u{1F4F1} **Mobile-friendly** - Lower startup cost on slower devices\n\n**Note:** Once loaded, pages remain in the DOM (hidden with CSS). The primary benefit is optimizing **initial load performance**, not ongoing memory usage.\n\nThis optimization happens automatically - no configuration needed.\n\n## Value Structure\n\nPages are container fields - they don't add nesting to your form values. Fields flatten to the root level:\n\n```typescript\n// Form config with pages\n{\n fields: [\n {\n key: 'page1',\n type: 'page',\n fields: [\n { key: 'firstName', type: 'input', value: '' },\n ],\n },\n {\n key: 'page2',\n type: 'page',\n fields: [\n { key: 'lastName', type: 'input', value: '' },\n ],\n },\n ],\n}\n\n// Resulting form value (flat structure)\n{\n firstName: 'John',\n lastName: 'Doe',\n // Note: page keys are NOT in the value\n}\n```\n\n## Nesting Restrictions\n\nPage fields can only be used at the top level of your form configuration. They **cannot** be nested inside:\n\n- Other page fields\n- Row fields\n- Group fields\n\nAttempting to nest pages will result in a validation error.\n\n## Allowed Children\n\nPages can contain:\n\n- Leaf fields (input, select, checkbox, etc.)\n- Row fields (for horizontal layouts)\n- Group fields (for nested data structures)\n\n## Conditional Visibility\n\nPages support the `logic` property to conditionally skip a page (hide it from the page navigation and progression) based on form state.\n\n```typescript\n{\n key: 'businessDetails',\n type: 'page',\n label: 'Business Details',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'notEquals',\n value: 'business',\n },\n }],\n fields: [\n { key: 'companyName', type: 'input', label: 'Company Name', value: '' },\n { key: 'taxId', type: 'input', label: 'Tax ID', value: '' },\n ],\n}\n```\n\nWhen a page is hidden, it is excluded from the multi-step navigation \u2014 users skip directly past it. Only `'hidden'` is supported as a logic type on containers.\n\nFor all available condition types and operators, see [Conditional Logic](../../dynamic-behavior/conditional-logic/overview/).\n\n## CSS Classes\n\nPage fields use these classes for styling:\n\n- `.df-page` - Applied to the page container\n- `.df-page-visible` - Applied to the currently visible page\n- `.df-page-hidden` - Applied to hidden pages\n- `.df-page-field` - Applied to the page field component"
183
- },
184
- {
185
- id: "form-rows",
186
- title: "Form Rows",
187
- category: "prebuilt",
188
- content: "Organize fields into horizontal rows for compact layouts. Rows display fields side-by-side.\n\n## Interactive Demo\n\n<iframe src=\"http://localhost:4201/#/examples/row\" class=\"example-frame\" title=\"Row Field Demo\"></iframe>\n\n## Basic Row\n\n```typescript\n{\n type: 'row',\n fields: [\n { key: 'firstName', type: 'input', label: 'First Name', value: '', col: 6 },\n { key: 'lastName', type: 'input', label: 'Last Name', value: '', col: 6 },\n ],\n}\n```\n\nRows flatten their children - they don't add nesting to form values. Use the `col` property to control field widths (see Column Sizing below).\n\n## Column Sizing\n\nControl field widths within rows using the `col` property:\n\n```typescript\n{\n type: 'row',\n fields: [\n { key: 'city', type: 'input', label: 'City', value: '', col: 6 },\n { key: 'state', type: 'select', label: 'State', value: '', col: 3 },\n { key: 'zip', type: 'input', label: 'ZIP', value: '', col: 3 },\n ],\n}\n```\n\nThe `col` property uses a 12-column grid system (like Bootstrap). In this example:\n\n- `city` takes 6/12 (50%) width\n- `state` takes 3/12 (25%) width\n- `zip` takes 3/12 (25%) width\n\n## Responsive Behavior\n\nRows automatically stack on small screens, making forms mobile-friendly without additional configuration.\n\n## Complete Example\n\nHere's a complete working example of a row field with multiple fields:\n\n```typescript\nimport { Component } from '@angular/core';\nimport { DynamicForm } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-address-form',\n imports: [DynamicForm],\n template: `<form [dynamic-form]=\"formConfig\"></form>\n >`,\n})\nexport class AddressFormComponent {\n formConfig = {\n fields: [\n {\n type: 'row',\n fields: [\n {\n key: 'firstName',\n type: 'input',\n label: 'First Name',\n value: '',\n required: true,\n col: 6,\n },\n {\n key: 'lastName',\n type: 'input',\n label: 'Last Name',\n value: '',\n required: true,\n col: 6,\n },\n ],\n },\n {\n key: 'email',\n type: 'input',\n label: 'Email Address',\n value: '',\n required: true,\n email: true,\n },\n {\n type: 'row',\n fields: [\n {\n key: 'city',\n type: 'input',\n label: 'City',\n value: '',\n required: true,\n col: 6,\n },\n {\n key: 'state',\n type: 'input',\n label: 'State',\n value: '',\n required: true,\n maxLength: 2,\n col: 3,\n },\n {\n key: 'zip',\n type: 'input',\n label: 'ZIP',\n value: '',\n required: true,\n pattern: /^\\d{5}$/,\n col: 3,\n },\n ],\n },\n ],\n };\n}\n```\n\nThis produces a flat form value (rows don't create nesting):\n\n```typescript\n{\n firstName: 'John',\n lastName: 'Doe',\n email: 'john.doe@example.com',\n city: 'Springfield',\n state: 'IL',\n zip: '62701'\n}\n```\n\n## Value Structure\n\nRows are layout containers - they don't add nesting to your form values. Fields flatten to the root level:\n\n```typescript\n// Form config with rows\n{\n fields: [\n {\n type: 'row',\n fields: [\n { key: 'firstName', type: 'input', value: '', col: 6 },\n { key: 'lastName', type: 'input', value: '', col: 6 },\n ],\n },\n ];\n}\n\n// Resulting form value (flat structure)\n{\n firstName: 'John',\n lastName: 'Doe',\n // Note: row itself is NOT in the value\n}\n```\n\n## Nesting Restrictions\n\nRow fields can be used within:\n\n- Pages (top-level container)\n- Groups (for layouts within grouped data)\n- Arrays (for layouts within array items)\n\nRows **cannot** be nested inside:\n\n- Other row fields\n\n## Allowed Children\n\nRows can contain:\n\n- Leaf fields (input, select, checkbox, etc.)\n- Group fields (for nested data structures within the row)\n- Array fields (for repeating sections within the row)\n\n## Conditional Visibility\n\nRow containers support the `logic` property to conditionally show or hide the entire row (and all its fields) based on form state.\n\n```typescript\n{\n key: 'contactRow',\n type: 'row',\n logic: [{\n type: 'hidden',\n condition: {\n type: 'fieldValue',\n fieldPath: 'contactPreference',\n operator: 'equals',\n value: 'none',\n },\n }],\n fields: [\n { key: 'phone', type: 'input', label: 'Phone', col: 6 },\n { key: 'email', type: 'input', label: 'Email', col: 6 },\n ],\n}\n```\n\nOnly `'hidden'` is supported as a logic type on containers. For all available condition types and operators, see [Conditional Logic](../../dynamic-behavior/conditional-logic/overview/)."
189
- },
190
- {
191
- id: "hidden-fields",
192
- title: "Hidden Fields",
193
- category: "prebuilt",
194
- content: "Hidden fields store values in the form model without rendering any visible UI. They're useful for persisting IDs, metadata, or other non-user-facing data.\n\n## Basic Usage\n\n```typescript\n{\n type: 'hidden',\n key: 'id',\n value: 'uuid-550e8400-e29b-41d4-a716-446655440000',\n}\n```\n\nThis adds `id` to the form value without rendering anything:\n\n```typescript\n{\n id: 'uuid-550e8400-e29b-41d4-a716-446655440000',\n // ... other fields\n}\n```\n\n## Supported Value Types\n\nHidden fields support scalar values and arrays of scalars:\n\n```typescript\n// String\n{ type: 'hidden', key: 'sessionId', value: 'abc123' }\n\n// Number\n{ type: 'hidden', key: 'version', value: 42 }\n\n// Boolean\n{ type: 'hidden', key: 'isActive', value: true }\n\n// String array\n{ type: 'hidden', key: 'tags', value: ['draft', 'review'] }\n\n// Number array\n{ type: 'hidden', key: 'tagIds', value: [1, 2, 3] }\n\n// Boolean array\n{ type: 'hidden', key: 'flags', value: [true, false, true] }\n```\n\n## Complete Example\n\nHere's a form that uses hidden fields to persist metadata during an update operation:\n\n```typescript\nimport { Component } from '@angular/core';\nimport { DynamicForm } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-edit-user-form',\n imports: [DynamicForm],\n template: `<form [dynamic-form]=\"formConfig\"></form>`,\n})\nexport class EditUserFormComponent {\n formConfig = {\n fields: [\n // Hidden fields for metadata\n {\n type: 'hidden',\n key: 'id',\n value: 'user-123',\n },\n {\n type: 'hidden',\n key: 'version',\n value: 3,\n },\n {\n type: 'hidden',\n key: 'roles',\n value: ['admin', 'editor'],\n },\n // Visible fields\n {\n key: 'name',\n type: 'input',\n label: 'Full Name',\n value: 'John Doe',\n required: true,\n },\n {\n key: 'email',\n type: 'input',\n label: 'Email',\n value: 'john@example.com',\n required: true,\n props: { type: 'email' },\n },\n {\n key: 'submit',\n type: 'submit',\n label: 'Save Changes',\n },\n ],\n };\n}\n```\n\nForm value on submission:\n\n```typescript\n{\n id: 'user-123',\n version: 3,\n roles: ['admin', 'editor'],\n name: 'John Doe',\n email: 'john@example.com'\n}\n```\n\n## Use Cases\n\n- Persisting record IDs for update operations\n- Tracking version numbers for optimistic concurrency\n- Passing context data (parent IDs, source references)\n- Storing computed values that shouldn't be user-editable\n- Including metadata in form submissions\n\n## Placement\n\nHidden fields can be placed:\n\n- At the top level of a form\n- Inside page fields\n- Inside group fields\n\n**Note:** Placing hidden fields inside row fields will generate a validation warning. Rows are meant for horizontal layouts, and since hidden fields don't render anything, placing them in rows serves no purpose. Place hidden fields outside of rows instead.\n\n## Type Safety\n\nHidden fields are fully type-safe:\n\n```typescript\nimport { HiddenField } from '@ng-forge/dynamic-forms';\n\n// Typed hidden field\nconst idField: HiddenField<string> = {\n type: 'hidden',\n key: 'id',\n value: 'abc123',\n};\n\n// Array type\nconst tagsField: HiddenField<number[]> = {\n type: 'hidden',\n key: 'tagIds',\n value: [1, 2, 3],\n};\n```\n\nUse the `isHiddenField` type guard to check field types at runtime:\n\n```typescript\nimport { isHiddenField } from '@ng-forge/dynamic-forms';\n\nif (isHiddenField(field)) {\n console.log(field.value); // Type-safe access to value\n}\n```\n\n## Value Exclusion\n\nThe `HiddenField` type (`type: 'hidden'`) is **not affected** by value exclusion. Hidden fields:\n\n- Have no reactive `hidden()` state (they don't render a component)\n- Are always included in submission output, regardless of `excludeValueIfHidden` settings\n\nThis is different from regular fields with `hidden: true` or fields hidden via conditional logic. Those fields have a reactive `hidden()` state that value exclusion checks.\n\nSee the **Value Exclusion** page under Recipes for full details.\n\n## Technical Details\n\n- **Componentless**: Hidden fields have no Angular component and render zero DOM elements\n- **Value handling**: Uses `valueHandling: 'include'`, so values are always included in form submissions\n- **Required value**: The `value` property is required (unlike most field types where it's optional)"
195
- },
196
- {
197
- id: "text-components",
198
- title: "Text Components",
199
- category: "prebuilt",
200
- content: "Display static or dynamic text content in forms. Text fields are **display-only** - they don't collect user input or contribute to form values.\n\n## Basic Text\n\nDisplay a paragraph of text:\n\n```typescript\n{\n key: 'instructions',\n type: 'text',\n label: 'Please review the terms and conditions before proceeding.',\n}\n```\n\n## HTML Element Types\n\nRender text as different HTML elements:\n\n```typescript\n{\n key: 'pageTitle',\n type: 'text',\n label: 'User Registration',\n props: {\n elementType: 'h1', // Options: 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span'\n },\n}\n```\n\n**Options**: `'p'` | `'h1'` | `'h2'` | `'h3'` | `'h4'` | `'h5'` | `'h6'` | `'span'`\n\n## With i18n\n\nText fields support dynamic content via Observables and Signals:\n\n```typescript\n{\n key: 'formInstructions',\n type: 'text',\n label: translationService.translate('form.instructions'), // Observable<string>\n props: {\n elementType: 'p',\n },\n}\n```\n\n## Styling with CSS Variables\n\nCustomize appearance using CSS custom properties:\n\n```css\n/* Global text styling */\n:root {\n --df-text-font-size: 1rem;\n --df-text-font-family: inherit;\n --df-text-font-weight: normal;\n --df-text-color: inherit;\n --df-text-line-height: 1.5;\n --df-text-text-align: inherit;\n --df-text-letter-spacing: normal;\n --df-text-text-decoration: none;\n --df-text-text-transform: none;\n --df-text-margin: 0;\n --df-text-padding: 0;\n}\n\n/* Element-specific styling */\n:root {\n --df-text-h1-font-size: 2rem;\n --df-text-h1-font-weight: bold;\n --df-text-h1-margin: 0;\n\n --df-text-h2-font-size: 1.75rem;\n --df-text-h2-font-weight: bold;\n --df-text-h2-margin: 0;\n\n --df-text-h3-font-size: 1.5rem;\n --df-text-h3-font-weight: bold;\n --df-text-h3-margin: 0;\n\n --df-text-h4-font-size: 1.25rem;\n --df-text-h4-font-weight: bold;\n --df-text-h4-margin: 0;\n\n --df-text-h5-font-size: 1.125rem;\n --df-text-h5-font-weight: bold;\n --df-text-h5-margin: 0;\n\n --df-text-h6-font-size: 1rem;\n --df-text-h6-font-weight: bold;\n --df-text-h6-margin: 0;\n\n --df-text-p-font-size: 1rem;\n --df-text-p-margin: 0;\n\n --df-text-span-display: inline;\n}\n```\n\n## CSS Classes\n\nText fields use these classes for styling:\n\n- `.df-text` - Applied to all text fields\n- `.df-text-{elementType}` - Element-specific class (e.g., `.df-text-h1`, `.df-text-p`)\n\n## Custom Classes\n\nAdd custom classes via the `className` property:\n\n```typescript\n{\n key: 'notice',\n type: 'text',\n label: 'Important Notice',\n className: 'my-custom-class highlight-text',\n props: {\n elementType: 'h2',\n },\n}\n```\n\n## Use Cases\n\n- Form instructions and help text\n- Section headings\n- Legal disclaimers\n- Status messages\n- Informational content between form fields"
201
- },
202
- {
203
- id: "schema-fields-field-types",
204
- title: "Schema Fields Field Types",
205
- category: "schema-fields",
206
- content: "Field types define form control behavior and rendering. Each UI integration (Material, Bootstrap, PrimeNG, Ionic) provides implementations for these core field types.\n\n**Note**: For nested form structures, use [group fields](../field-types#group) instead of dot notation in keys.\n\n## Core Field Types\n\n### input\n\nText-based input with HTML5 type support.\n\n```typescript\n{\n key: 'email',\n type: 'input',\n value: '',\n label: 'Email',\n required: true,\n email: true,\n props: {\n type: 'email', // 'text' | 'email' | 'password' | 'number' | 'tel' | 'url'\n placeholder: 'user@example.com',\n }\n}\n```\n\n**Core Props:**\n\n- `type`: HTML input type (`'text'` | `'email'` | `'password'` | `'number'` | `'tel'` | `'url'`)\n- `placeholder`: Input placeholder text\n\n**Note:** UI integrations may extend props with additional features like `hint`, `appearance`, etc. See your specific integration's documentation.\n\n### select\n\nSingle or multi-select dropdown.\n\n```typescript\n{\n key: 'country',\n type: 'select',\n value: '',\n label: 'Country',\n required: true,\n options: [\n { value: 'us', label: 'United States' },\n { value: 'uk', label: 'United Kingdom' },\n ],\n props: {\n placeholder: 'Select country',\n }\n}\n```\n\n**Core Properties:**\n\n- `options`: Array of `{ value: T, label: string }` objects (at field level, not in props)\n\n**Core Props:**\n\n- `placeholder`: Placeholder text when no value selected\n\n**Note:** UI integrations may extend with additional props like `multiple`, `clearable`, etc. Check your specific integration's documentation.\n\n### checkbox\n\nBoolean toggle control.\n\n```typescript\n{\n key: 'newsletter',\n type: 'checkbox',\n value: false,\n label: 'Subscribe to newsletter',\n}\n```\n\n**Note:** UI integrations may extend with additional props like `hint`. The core checkbox has no required props.\n\n### radio\n\nSingle selection from multiple options.\n\n```typescript\n{\n key: 'plan',\n type: 'radio',\n value: '',\n label: 'Subscription Plan',\n required: true,\n options: [\n { value: 'free', label: 'Free' },\n { value: 'pro', label: 'Pro - $10/month' },\n { value: 'enterprise', label: 'Enterprise - $50/month' },\n ],\n}\n```\n\n**Core Properties:**\n\n- `options`: Array of `{ value: string, label: string }` objects (at field level, not in props)\n\n### textarea\n\nMulti-line text input.\n\n```typescript\n{\n key: 'bio',\n type: 'textarea',\n value: '',\n label: 'Biography',\n maxLength: 500,\n props: {\n placeholder: 'Tell us about yourself',\n rows: 4,\n }\n}\n```\n\n**Core Props:**\n\n- `placeholder`: Placeholder text\n- `rows`: Number of visible text rows\n\n### datepicker\n\nDate selection control (requires UI integration).\n\n```typescript\n{\n key: 'birthDate',\n type: 'datepicker',\n label: 'Birth Date',\n value: null,\n required: true,\n minDate: new Date(1900, 0, 1), // optional\n maxDate: new Date(), // optional\n props: {\n placeholder: 'Select your birth date',\n }\n}\n```\n\n**Core Properties (all optional):**\n\n- `minDate`: Minimum selectable date (at field level) - `Date | string | null`\n- `maxDate`: Maximum selectable date (at field level) - `Date | string | null`\n- `startAt`: Initial calendar view date - `Date | null`\n\n**Core Props:**\n\n- `placeholder`: Placeholder text\n- `format`: Date format string (UI-integration specific)\n\n### slider\n\nNumeric range selection (requires UI integration).\n\n```typescript\n{\n key: 'volume',\n type: 'slider',\n label: 'Volume',\n value: 50,\n minValue: 0, // optional\n maxValue: 100, // optional\n step: 5, // optional\n}\n```\n\n**Core Properties (all optional):**\n\n- `minValue`: Minimum slider value (at field level)\n- `maxValue`: Maximum slider value (at field level)\n- `step`: Step increment value (at field level)\n\n### toggle\n\nBoolean switch control (requires UI integration). Similar to checkbox but with switch UI.\n\n```typescript\n{\n key: 'darkMode',\n type: 'toggle',\n label: 'Enable Dark Mode',\n value: false,\n}\n```\n\n### multi-checkbox\n\nMultiple selection from a list of checkboxes. Value is an array of selected values.\n\n```typescript\n{\n key: 'interests',\n type: 'multi-checkbox',\n label: 'Interests',\n value: [], // Array of selected values\n options: [\n { value: 'tech', label: 'Technology' },\n { value: 'sports', label: 'Sports' },\n { value: 'music', label: 'Music' },\n ],\n}\n```\n\n**Core Properties:**\n\n- `options`: Array of `{ value: T, label: string }` objects (at field level, not in props)\n\n### text\n\nDisplay-only text content (not a form control). Useful for instructions, headers, or dynamic content.\n\n```typescript\n{\n key: 'instructions',\n type: 'text',\n label: 'Please fill out all required fields',\n props: {\n elementType: 'p', // 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span'\n },\n}\n```\n\n**Core Props:**\n\n- `elementType`: HTML element to render (`'p'` | `'h1'` - `'h6'` | `'span'`)\n\n**Note:** Text fields don't have a `value` - they display the `label` content.\n\n## Validation\n\nFields integrate with Angular's signal forms validation system. ng-forge provides shorthand syntax for common validators (e.g., `required: true`) with comprehensive customizability via the `validators` property. See [Validation](../validation/basics) for details.\n\n```typescript\n{\n key: 'username',\n type: 'input',\n value: '',\n label: 'Username',\n required: true,\n minLength: 3,\n maxLength: 20,\n pattern: '^[a-zA-Z0-9_]+$',\n}\n```\n\n## Props vs Meta\n\nWhen configuring fields, there are two distinct ways to customize behavior: `props` and `meta`. Understanding the difference is important for proper field configuration.\n\n### Props (Component Properties)\n\n`props` are **UI library-specific configuration** passed to the field component. They control the behavior and appearance of the UI library's widget.\n\n```typescript\n{\n key: 'country',\n type: 'select',\n label: 'Country',\n props: {\n // Material-specific: controls form field appearance\n appearance: 'outline',\n // PrimeNG-specific: enables filtering\n filter: true,\n showClear: true,\n },\n}\n```\n\n**Characteristics of props:**\n\n- UI library-specific (different props for Material vs PrimeNG)\n- Control component behavior (multiple selection, filtering, etc.)\n- Affect visual appearance (appearance, size, variant)\n- Passed to wrapper components, not native elements\n- Defined by each UI adapter's type definitions\n\n### Meta (Native Element Attributes)\n\n`meta` contains **native HTML attributes** that should be applied to the underlying DOM element. These are framework-agnostic attributes used for accessibility, testing, and browser features.\n\n```typescript\n{\n key: 'email',\n type: 'input',\n label: 'Email',\n meta: {\n // HTML autocomplete for browser autofill\n autocomplete: 'email',\n // Keyboard input mode hint\n inputmode: 'email',\n // Testing attribute\n 'data-testid': 'email-input',\n // Analytics tracking\n 'data-analytics': 'email-field',\n // ARIA attributes\n 'aria-describedby': 'email-help',\n },\n}\n```\n\n**Characteristics of meta:**\n\n- Framework-agnostic (same across all UI libraries)\n- Applied to native DOM elements (input, select, textarea)\n- Used for accessibility (`aria-*`), testing (`data-*`), and browser features (`autocomplete`)\n- Important for screen readers, automated testing, and browser autofill\n- Defined in core library types\n\n### When to Use Each\n\n| Use Case | Use `props` | Use `meta` |\n| ------------------------------------- | ----------- | ---------- |\n| UI appearance (size, variant) | \u2705 | \u274C |\n| Component behavior (multiple, filter) | \u2705 | \u274C |\n| Browser autofill (`autocomplete`) | \u274C | \u2705 |\n| Testing IDs (`data-testid`) | \u274C | \u2705 |\n| Accessibility (`aria-*`) | \u274C | \u2705 |\n| Analytics tracking (`data-*`) | \u274C | \u2705 |\n| Input mode hints (`inputmode`) | \u274C | \u2705 |\n\n### Complete Example\n\n```typescript\n{\n key: 'email',\n type: 'input',\n label: 'Email Address',\n required: true,\n email: true,\n // UI library-specific configuration\n props: {\n type: 'email',\n appearance: 'outline', // Material-specific\n hint: 'We will never share your email',\n },\n // Native HTML attributes\n meta: {\n autocomplete: 'email',\n inputmode: 'email',\n 'data-testid': 'registration-email',\n },\n}\n```\n\n## UI Integrations\n\nUI framework integrations extend field types with framework-specific styling and features while maintaining the same configuration API.\n\n- [Material Design](../ui-libs-integrations/material) - Material Design components\n- [Bootstrap](../ui-libs-integrations/bootstrap) - Bootstrap styling\n- [PrimeNG](../ui-libs-integrations/primeng) - PrimeNG components\n- [Ionic](../ui-libs-integrations/ionic) - Ionic mobile components"
207
- },
208
- {
209
- id: "schema-validation-angular-schema",
210
- title: "Schema Validation Angular Schema",
211
- category: "schema-validation",
212
- content: "Angular's signal forms include a native `Schema<T>` API for form-level validation. This approach requires no additional dependencies and integrates seamlessly with Dynamic Forms.\n\n## Raw Callback Pattern (Recommended)\n\nFor maximum simplicity, pass Angular's schema callback directly without any wrapper:\n\n```typescript\nimport { FormConfig } from '@ng-forge/dynamic-forms';\nimport { validateTree } from '@angular/forms/signals';\n\nconst config = {\n // Raw callback - no wrapper needed!\n schema: (path) => {\n validateTree(path, (ctx) => {\n const { password, confirmPassword } = ctx.value();\n if (password !== confirmPassword) {\n return [{ kind: 'passwordMismatch', fieldTree: ctx.fieldTreeOf(path).confirmPassword }];\n }\n return null;\n });\n },\n fields: [\n { key: 'password', type: 'input', label: 'Password', required: true, props: { type: 'password' } },\n {\n key: 'confirmPassword',\n type: 'input',\n label: 'Confirm Password',\n required: true,\n validationMessages: { passwordMismatch: 'Passwords must match' },\n props: { type: 'password' },\n },\n { key: 'submit', type: 'submit', label: 'Register' },\n ],\n} as const satisfies FormConfig;\n```\n\nThis pattern gives you full access to Angular's validation APIs including `validateTree`, `validate`, and `required`.\n\n## Combining Field and Schema Validation\n\nField-level validators (like `required`, `minLength`) run **first**, then the schema callback runs for cross-field validation. Both work together seamlessly.\n\n## Using schema() Wrapper\n\nAlternatively, you can use Angular's `schema()` function to wrap your callback:\n\n```typescript\nimport { schema, required, validate } from '@angular/forms/signals';\n```\n\nDefine your schema with cross-field validation:\n\n```typescript\ninterface PasswordForm {\n password: string;\n confirmPassword: string;\n}\n\nconst passwordSchema = schema<PasswordForm>(({ value }) =>\n validate(value.password === value.confirmPassword, { confirmPassword: { passwordMismatch: true } }),\n);\n```\n\n## Using with Dynamic Forms\n\nPass the Angular schema directly to your form configuration:\n\n```typescript\nimport { FormConfig } from '@ng-forge/dynamic-forms';\nimport { schema, validate } from '@angular/forms/signals';\n\ninterface PasswordForm {\n password: string;\n confirmPassword: string;\n}\n\nconst config = {\n schema: schema<PasswordForm>(({ value }) =>\n validate(value.password === value.confirmPassword, { confirmPassword: { passwordMismatch: true } }),\n ),\n fields: [\n {\n key: 'password',\n type: 'input',\n label: 'Password',\n required: true,\n minLength: 8,\n props: { type: 'password' },\n },\n {\n key: 'confirmPassword',\n type: 'input',\n label: 'Confirm Password',\n required: true,\n validationMessages: {\n passwordMismatch: 'Passwords must match',\n },\n props: { type: 'password' },\n },\n {\n key: 'submit',\n type: 'submit',\n label: 'Register',\n },\n ],\n} as const satisfies FormConfig;\n```\n\n## Schema API Reference\n\n### `schema<T>(validator)`\n\nCreates a form-level schema validator.\n\n```typescript\nconst mySchema = schema<MyFormType>(({ value, touched, dirty }) => {\n // value: current form value\n // touched: whether form has been touched\n // dirty: whether form has been modified\n return validate(/* condition */, /* errors */);\n});\n```\n\n### `validate(condition, errors)`\n\nReturns validation errors when condition is false.\n\n```typescript\nvalidate(endDate > startDate, { endDate: { invalidRange: true } });\n```\n\n### `required(path)`\n\nMarks a field as conditionally required.\n\n```typescript\nschema<MyForm>(({ value }) => (value.hasEndDate ? required('endDate') : null));\n```\n\n### Combining Validators\n\nReturn multiple validation results:\n\n```typescript\nschema<MyForm>(({ value }) => [\n validate(value.password === value.confirmPassword, { confirmPassword: { passwordMismatch: true } }),\n validate(value.endDate > value.startDate, { endDate: { invalidRange: true } }),\n]);\n```\n\n## Examples\n\n### Date Range Validation\n\n```typescript\ninterface DateRangeForm {\n startDate: string;\n endDate: string;\n}\n\nconst dateRangeSchema = schema<DateRangeForm>(({ value }) =>\n validate(!value.startDate || !value.endDate || new Date(value.endDate) >= new Date(value.startDate), { endDate: { invalidRange: true } }),\n);\n\nconst config = {\n schema: dateRangeSchema,\n fields: [\n {\n key: 'startDate',\n type: 'datepicker',\n label: 'Start Date',\n required: true,\n },\n {\n key: 'endDate',\n type: 'datepicker',\n label: 'End Date',\n required: true,\n validationMessages: {\n invalidRange: 'End date must be after start date',\n },\n },\n ],\n} as const satisfies FormConfig;\n```\n\n### Conditional Required Fields\n\n```typescript\ninterface ContactForm {\n preferredContact: 'email' | 'phone';\n email: string;\n phone: string;\n}\n\nconst contactSchema = schema<ContactForm>(({ value }) => [\n value.preferredContact === 'email' && !value.email ? validate(false, { email: { required: true } }) : null,\n value.preferredContact === 'phone' && !value.phone ? validate(false, { phone: { required: true } }) : null,\n]);\n```\n\n### Complex Business Rules\n\n```typescript\ninterface OrderForm {\n quantity: number;\n discount: number;\n discountType: 'percentage' | 'fixed';\n total: number;\n}\n\nconst orderSchema = schema<OrderForm>(({ value }) => {\n const errors = [];\n\n // Percentage discount can't exceed 100%\n if (value.discountType === 'percentage' && value.discount > 100) {\n errors.push(\n validate(false, {\n discount: { maxPercentage: true },\n }),\n );\n }\n\n // Fixed discount can't exceed total\n if (value.discountType === 'fixed' && value.discount > value.total) {\n errors.push(\n validate(false, {\n discount: { exceedsTotal: true },\n }),\n );\n }\n\n return errors;\n});\n```\n\n## Best Practices\n\n**Keep schemas focused:**\n\n```typescript\n// Good - single responsibility\nconst passwordMatchSchema = schema<PasswordForm>(({ value }) =>\n validate(value.password === value.confirmPassword, { confirmPassword: { passwordMismatch: true } }),\n);\n\n// Avoid - too many concerns\nconst everythingSchema = schema<BigForm>(({ value }) => [\n // 10+ validations...\n]);\n```\n\n**Use field validators for simple cases:**\n\n```typescript\n// Use field validators when possible\n{\n key: 'email',\n required: true,\n email: true,\n}\n\n// Use schema for cross-field only\nschema: schema<Form>(({ value }) =>\n validate(value.email !== value.alternateEmail, {\n alternateEmail: { sameAsEmail: true }\n })\n)\n```\n\n## When to Use Standard Schema Instead\n\nConsider [Standard Schema (Zod)](../zod) if you:\n\n- Already have Zod schemas from an API or shared library\n- Need the same validation in Node.js backend\n- Want automatic TypeScript type inference from schemas\n- Are using OpenAPI-generated schemas\n\n## Related\n\n- **[Schema Validation Overview](../overview)** - When to use form-level validation\n- **[Standard Schema (Zod)](../zod)** - Alternative using Zod/Valibot\n- **[Field Validation](../../validation/basics/)** - Individual field validators"
213
- },
214
- {
215
- id: "schema-validation-overview",
216
- title: "Schema Validation Overview",
217
- category: "schema-validation",
218
- content: "Form-level schema validation enables cross-field validation rules that examine the entire form state. While field-level validators check individual fields, schema validators validate relationships between multiple fields.\n\n## When to Use Schema Validation\n\nSchema validation excels at:\n\n- **Password confirmation** - Ensure two fields match\n- **Date ranges** - Validate end date is after start date\n- **Conditional requirements** - Complex business rules across fields\n- **Data consistency** - Ensure related fields are logically consistent\n\n## Available Approaches\n\nDynamic Forms supports two approaches to form-level validation:\n\n| Approach | Best For | Wrapper Required |\n| ----------------------------------- | -------------------------------------- | ------------------------ |\n| [Angular Schema](../angular-schema) | Angular-only projects, full API access | No (raw callback) |\n| [Standard Schema](../zod) | Reusing Zod/Valibot schemas, OpenAPI | Yes (`standardSchema()`) |\n\n### Angular Schema\n\nUses Angular's native schema APIs from signal forms. Best when:\n\n- Your project is Angular-only\n- You want zero additional dependencies\n- Validation logic is specific to this form\n\n```typescript\nimport { validateTree } from '@angular/forms/signals';\n\nconst config = {\n // Raw callback - no wrapper needed!\n schema: (path) => {\n validateTree(path, (ctx) => {\n const { password, confirmPassword } = ctx.value();\n if (password !== confirmPassword) {\n return [{ kind: 'passwordMismatch', fieldTree: ctx.fieldTreeOf(path).confirmPassword }];\n }\n return null;\n });\n },\n fields: [\n /* ... */\n ],\n};\n```\n\n### Standard Schema (Zod, Valibot, ArkType)\n\nUses the [Standard Schema](https://standardschema.dev) spec for cross-library compatibility. Best when:\n\n- You have existing Zod/Valibot schemas\n- You're reusing OpenAPI-generated schemas\n- You want the same validation on frontend and backend\n\n```typescript\nimport { z } from 'zod';\nimport { standardSchema } from '@ng-forge/dynamic-forms/schema';\n\nconst passwordSchema = z\n .object({\n password: z.string().min(8),\n confirmPassword: z.string(),\n })\n .refine((data) => data.password === data.confirmPassword, { message: 'Passwords must match', path: ['confirmPassword'] });\n\nconst config = {\n schema: standardSchema(passwordSchema),\n fields: [\n /* ... */\n ],\n};\n```\n\n## Comparison\n\n| Feature | Angular Schema | Standard Schema |\n| ---------------- | ------------------------ | --------------------------------- |\n| Dependencies | None (Angular core) | Schema library (Zod, etc.) |\n| Type inference | Manual | Automatic from schema |\n| Cross-platform | No | Yes (same schema in Node.js) |\n| OpenAPI compat | No | Yes (via zod-openapi, etc.) |\n| Learning curve | Familiar to Angular devs | Requires schema library knowledge |\n| Wrapper required | No (raw callback) | Yes (`standardSchema()`) |\n\n## How It Works\n\n1. Define your schema (Angular or Standard Schema)\n2. Add it to your form configuration via the `schema` property\n3. The form validates against the schema on every change\n4. Schema errors are mapped to the appropriate fields\n\n## Next Steps\n\n- **[Angular Schema](../angular-schema)** - Native Angular approach\n- **[Standard Schema (Zod)](../zod)** - Use Zod, Valibot, or ArkType\n- **[Field Validation](../../validation/basics/)** - Individual field validators"
219
- },
220
- {
221
- id: "schema-validation-zod",
222
- title: "Schema Validation Zod",
223
- category: "schema-validation",
224
- content: "Use [Zod](https://zod.dev) schemas to validate your dynamic forms. This lets you reuse existing schemas, share validation logic between frontend and backend, and leverage Zod's powerful cross-field validation like `.refine()`.\n\n## Installation\n\n```bash\nnpm install zod\n```\n\n## Basic Usage\n\n```typescript\nimport { z } from 'zod';\nimport { standardSchema } from '@ng-forge/dynamic-forms/schema';\n\nconst userSchema = z.object({\n email: z.string().email('Invalid email address'),\n password: z.string().min(8, 'Password must be at least 8 characters'),\n});\n\nconst config = {\n schema: standardSchema(userSchema),\n fields: [\n { key: 'email', type: 'input', label: 'Email', props: { type: 'email' } },\n { key: 'password', type: 'input', label: 'Password', props: { type: 'password' } },\n { key: 'submit', type: 'submit', label: 'Register' },\n ],\n} as const satisfies FormConfig;\n```\n\nThe `standardSchema()` wrapper tells Dynamic Forms to use Zod for validation. Errors are automatically mapped to the corresponding form fields.\n\n## Live Demo\n\nTry the password confirmation form with Zod validation:\n\n<iframe src=\"http://localhost:4201/#/examples/zod-schema-validation\" class=\"example-frame\" title=\"Zod Validation Demo\"></iframe>\n\n## Cross-Field Validation\n\nThe main reason to use Zod is cross-field validation - rules that depend on multiple fields.\n\n### Password Confirmation with `.refine()`\n\n```typescript\nconst passwordSchema = z\n .object({\n password: z.string().min(8, 'Password must be at least 8 characters'),\n confirmPassword: z.string(),\n })\n .refine((data) => data.password === data.confirmPassword, {\n message: 'Passwords must match',\n path: ['confirmPassword'], // Error appears on this field\n });\n```\n\n### Date Range Validation\n\n```typescript\nconst dateRangeSchema = z\n .object({\n startDate: z.string(),\n endDate: z.string(),\n })\n .refine((data) => new Date(data.endDate) >= new Date(data.startDate), {\n message: 'End date must be after start date',\n path: ['endDate'],\n });\n```\n\n### Multiple Cross-Field Rules with `.superRefine()`\n\n```typescript\nconst registrationSchema = z\n .object({\n password: z.string().min(8),\n confirmPassword: z.string(),\n email: z.string().email(),\n confirmEmail: z.string().email(),\n })\n .superRefine((data, ctx) => {\n if (data.password !== data.confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'Passwords must match',\n path: ['confirmPassword'],\n });\n }\n if (data.email !== data.confirmEmail) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'Emails must match',\n path: ['confirmEmail'],\n });\n }\n });\n```\n\n### Conditional Required Fields\n\n```typescript\nconst contactSchema = z\n .object({\n contactMethod: z.enum(['email', 'phone']),\n email: z.string().optional(),\n phone: z.string().optional(),\n })\n .refine(\n (data) => {\n if (data.contactMethod === 'email') return !!data.email;\n if (data.contactMethod === 'phone') return !!data.phone;\n return true;\n },\n (data) => ({\n message: `${data.contactMethod} is required`,\n path: [data.contactMethod],\n }),\n );\n```\n\n## Reusing Schemas\n\n### From OpenAPI or Backend\n\n```typescript\n// Reuse schemas generated from OpenAPI specs\nimport { UserCreateSchema } from './generated/schemas';\n\nconst config = {\n schema: standardSchema(UserCreateSchema),\n fields: [\n { key: 'username', type: 'input', label: 'Username' },\n { key: 'email', type: 'input', label: 'Email' },\n { key: 'password', type: 'input', label: 'Password', props: { type: 'password' } },\n ],\n} as const satisfies FormConfig;\n```\n\n### Shared Validation Logic\n\n```typescript\n// schemas/user.schema.ts - shared with backend\nexport const userSchema = z.object({\n email: z.string().email('Invalid email'),\n password: z.string().min(8, 'Password too short'),\n});\n\n// frontend form\nimport { userSchema } from '@shared/schemas';\n\nconst config = {\n schema: standardSchema(userSchema),\n fields: [\n { key: 'email', type: 'input', label: 'Email' },\n { key: 'password', type: 'input', label: 'Password', props: { type: 'password' } },\n ],\n} as const satisfies FormConfig;\n```\n\n## How Errors Work\n\nZod errors automatically map to form fields via the `path` property:\n\n```typescript\n// When validation fails, Zod produces:\n{\n issues: [{ path: ['confirmPassword'], message: 'Passwords must match' }];\n}\n\n// Dynamic Forms maps this to Angular's form errors:\nform.controls.confirmPassword.errors;\n// \u2192 { 'Passwords must match': true }\n```\n\n## Combining with Field Validators\n\nUse Zod for cross-field rules, field-level validators for single-field rules:\n\n```typescript\nconst config = {\n // Zod handles cross-field validation\n schema: standardSchema(\n z.object({ password: z.string(), confirm: z.string() }).refine((data) => data.password === data.confirm, {\n message: 'Passwords must match',\n path: ['confirm'],\n }),\n ),\n\n // Field validators handle single-field rules\n fields: [\n { key: 'password', type: 'input', label: 'Password', required: true, minLength: 8 },\n { key: 'confirm', type: 'input', label: 'Confirm Password', required: true },\n ],\n} as const satisfies FormConfig;\n```\n\n## Other Schema Libraries\n\nThe same approach works with Valibot and ArkType:\n\n```typescript\n// Valibot\nimport * as v from 'valibot';\nconst schema = v.object({ email: v.pipe(v.string(), v.email()) });\nconst config = { schema: standardSchema(schema), fields: [...] };\n\n// ArkType\nimport { type } from 'arktype';\nconst schema = type({ email: 'email' });\nconst config = { schema: standardSchema(schema), fields: [...] };\n```\n\n## Related\n\n- **[Schema Validation Overview](../overview)** - When to use form-level validation\n- **[Angular Schema](../angular-schema)** - Native Angular approach without dependencies\n- **[Field Validation](../../validation/basics/)** - Individual field validators"
225
- },
226
- {
227
- id: "bootstrap",
228
- title: "Bootstrap",
229
- category: "ui-libs-integrations",
230
- content: "Modern Bootstrap 5 field components for ng-forge dynamic forms, built with native HTML elements and Bootstrap CSS.\n\n------------ | -------------- | ------- | ---------------------------- |\n| `size` | `'sm' \\| 'lg'` | - | Bootstrap size class |\n| `floatingLabel` | `boolean` | `false` | Enable floating label design |\n\n---\n\n## Type Augmentation\n\nImporting this package automatically extends `@ng-forge/dynamic-forms` with Bootstrap-specific field types via TypeScript module augmentation. If you need type safety in a file without importing specific exports, use a bare import:\n\n```typescript\nimport '@ng-forge/dynamic-forms-bootstrap';\n```\n\n---\n\n## Complete Form Example\n\nHere's a full registration form showcasing multiple Bootstrap field types:\n\n<iframe src=\"http://localhost:4204/#/examples/complete-form\" class=\"example-frame\" title=\"Complete Form Demo\"></iframe>\n\nThis example demonstrates:\n\n- Text inputs with validation\n- Select dropdowns\n- Checkboxes and switches\n- Radio buttons\n- Date inputs\n- Range sliders\n- Multi-checkbox selections\n- Form submission\n\n---\n\n## Field Types Reference\n\nComplete reference for all Bootstrap field types with comprehensive validation, native HTML5 features, and Bootstrap styling.\n\n### Text Input Fields\n\nText input fields provide user-friendly text entry with Bootstrap form styling.\n\n#### Input\n\nText input field with HTML5 type support and optional floating labels.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4204/#/examples/input\" class=\"example-frame\" title=\"Input Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'email',\n type: 'input',\n label: 'Email Address',\n value: '',\n required: true,\n email: true,\n props: {\n type: 'email',\n floatingLabel: true,\n placeholder: 'Enter your email',\n hint: 'We will never share your email',\n },\n}\n```\n\n**Field Properties:**\n\n| Property | Type | Description |\n| ------------- | ------------------ | ---------------------------------- |\n| `key` | `string` | Unique field identifier (required) |\n| `type` | `'input'` | Field type (required) |\n| `value` | `string \\| number` | Initial value |\n| `label` | `string` | Field label |\n| `placeholder` | `string` | Placeholder text |\n| `required` | `boolean` | Mark field as required |\n| `disabled` | `boolean` | Disable the field |\n| `readonly` | `boolean` | Make field read-only |\n\n**Validation Properties:**\n\n| Property | Type | Description |\n| ----------- | ------------------ | --------------------------------- |\n| `email` | `boolean` | Email format validation |\n| `minLength` | `number` | Minimum character length |\n| `maxLength` | `number` | Maximum character length |\n| `min` | `number` | Minimum value (for number inputs) |\n| `max` | `number` | Maximum value (for number inputs) |\n| `pattern` | `string \\| RegExp` | RegEx pattern validation |\n\n**Props (Bootstrap-Specific):**\n\n| Prop | Type | Default | Description |\n| ----------------- | --------------------------------------------------------------- | -------- | ---------------------------- |\n| `type` | `'text' \\| 'email' \\| 'password' \\| 'number' \\| 'tel' \\| 'url'` | `'text'` | HTML input type |\n| `size` | `'sm' \\| 'lg'` | - | Bootstrap size class |\n| `floatingLabel` | `boolean` | `false` | Enable floating label design |\n| `plaintext` | `boolean` | `false` | Render as plaintext |\n| `hint` | `string` | - | Helper text below input |\n| `validFeedback` | `string` | - | Success message when valid |\n| `invalidFeedback` | `string` | - | Error message when invalid |\n\n#### Textarea\n\nMulti-line text input field with Bootstrap form styling.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4204/#/examples/textarea\" class=\"example-frame\" title=\"Textarea Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'bio',\n type: 'textarea',\n value: '',\n label: 'Biography',\n placeholder: 'Tell us about yourself',\n required: true,\n props: {\n rows: 4,\n floatingLabel: true,\n },\n}\n```\n\n**Props (Bootstrap-Specific):**\n\n| Prop | Type | Default | Description |\n| --------------- | -------------- | ------- | ---------------------------- |\n| `rows` | `number` | `3` | Number of visible rows |\n| `size` | `'sm' \\| 'lg'` | - | Bootstrap size class |\n| `floatingLabel` | `boolean` | `false` | Enable floating label design |\n| `hint` | `string` | - | Helper text below textarea |\n\n---\n\n### Selection Fields\n\nSelection fields enable users to choose from predefined options.\n\n#### Select\n\nDropdown selection field with native HTML select element. Supports both single and multi-select modes.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4204/#/examples/select\" class=\"example-frame\" title=\"Select Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'country',\n type: 'select',\n value: '',\n label: 'Country',\n required: true,\n options: [\n { value: 'us', label: 'United States' },\n { value: 'uk', label: 'United Kingdom' },\n { value: 'ca', label: 'Canada' },\n ],\n props: {\n floatingLabel: true,\n },\n}\n```\n\n**Props (Bootstrap-Specific):**\n\n| Prop | Type | Default | Description |\n| --------------- | -------------- | ------- | ---------------------------- |\n| `multiple` | `boolean` | `false` | Enable multi-select |\n| `size` | `'sm' \\| 'lg'` | - | Bootstrap size class |\n| `floatingLabel` | `boolean` | `false` | Enable floating label design |\n| `hint` | `string` | - | Helper text below select |\n\n#### Radio\n\nRadio button group for selecting a single option with optional button group styling.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4204/#/examples/radio\" class=\"example-frame\" title=\"Radio Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'size',\n type: 'radio',\n value: '',\n label: 'Select Size',\n required: true,\n options: [\n { value: 'small', label: 'Small' },\n { value: 'medium', label: 'Medium' },\n { value: 'large', label: 'Large' },\n ],\n}\n```\n\n**Props (Bootstrap-Specific):**\n\n| Prop | Type | Default | Description |\n| ------------- | -------------- | ------- | ----------------------------------------- |\n| `inline` | `boolean` | `false` | Display options inline |\n| `reverse` | `boolean` | `false` | Reverse label and input position |\n| `buttonGroup` | `boolean` | `false` | Render as Bootstrap button group |\n| `buttonSize` | `'sm' \\| 'lg'` | - | Button size (when buttonGroup is enabled) |\n| `hint` | `string` | - | Helper text below radio group |\n\n#### Checkbox\n\nBoolean checkbox control for single true/false selections.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4204/#/examples/checkbox\" class=\"example-frame\" title=\"Checkbox Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'terms',\n type: 'checkbox',\n value: false,\n label: 'I accept the terms and conditions',\n required: true,\n}\n```\n\n**Props (Bootstrap-Specific):**\n\n| Prop | Type | Default | Description |\n| --------- | --------- | ------- | -------------------------------- |\n| `switch` | `boolean` | `false` | Render as Bootstrap switch |\n| `inline` | `boolean` | `false` | Inline layout |\n| `reverse` | `boolean` | `false` | Reverse label and input position |\n\n#### Multi-Checkbox\n\nMultiple checkbox selection field for choosing multiple options.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4204/#/examples/multi-checkbox\" class=\"example-frame\" title=\"Multi-Checkbox Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'interests',\n type: 'multi-checkbox',\n value: [],\n label: 'Select Your Interests',\n required: true,\n options: [\n { value: 'sports', label: 'Sports' },\n { value: 'music', label: 'Music' },\n { value: 'reading', label: 'Reading' },\n { value: 'travel', label: 'Travel' },\n ],\n}\n```\n\n**Props (Bootstrap-Specific):**\n\n| Prop | Type | Default | Description |\n| --------- | --------- | ------- | -------------------------------- |\n| `switch` | `boolean` | `false` | Render checkboxes as switches |\n| `inline` | `boolean` | `false` | Display options inline |\n| `reverse` | `boolean` | `false` | Reverse label and input position |\n| `hint` | `string` | - | Helper text below checkbox group |\n\n---\n\n### Interactive Fields\n\nInteractive fields provide advanced user input controls.\n\n#### Toggle\n\nSlide toggle switch for boolean on/off selections (renders as Bootstrap switch).\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4204/#/examples/toggle\" class=\"example-frame\" title=\"Toggle Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'notifications',\n type: 'toggle',\n value: false,\n label: 'Enable notifications',\n}\n```\n\n**Props (Bootstrap-Specific):**\n\n| Prop | Type | Default | Description |\n| --------- | --------- | ------- | -------------------------------- |\n| `inline` | `boolean` | `false` | Inline layout |\n| `reverse` | `boolean` | `false` | Reverse label and input position |\n\n#### Slider\n\nNative HTML5 range input for selecting values from a numeric range.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4204/#/examples/slider\" class=\"example-frame\" title=\"Slider Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'volume',\n type: 'slider',\n value: 50,\n label: 'Volume',\n minValue: 0,\n maxValue: 100,\n step: 1,\n props: {\n showValue: true,\n },\n}\n```\n\n**Props (Bootstrap-Specific):**\n\n| Prop | Type | Default | Description |\n| ----------- | --------- | ------- | ------------------------ |\n| `showValue` | `boolean` | `false` | Display current value |\n| `hint` | `string` | - | Helper text below slider |\n\n#### Datepicker\n\nNative HTML5 date input with Bootstrap styling.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4204/#/examples/datepicker\" class=\"example-frame\" title=\"Datepicker Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'birthdate',\n type: 'datepicker',\n value: null,\n label: 'Date of Birth',\n required: true,\n props: {\n min: '1900-01-01',\n max: '2025-12-31',\n },\n}\n```\n\n**Field Properties:**\n\n| Property | Type | Description |\n| --------- | ------------------------ | ----------------------- |\n| `minDate` | `Date \\| string \\| null` | Minimum selectable date |\n| `maxDate` | `Date \\| string \\| null` | Maximum selectable date |\n\n**Props (Bootstrap-Specific):**\n\n| Prop | Type | Default | Description |\n| ----------------- | -------------------------------------- | ------- | ------------------------------------------ |\n| `size` | `'sm' \\| 'lg'` | - | Bootstrap size class |\n| `floatingLabel` | `boolean` | `false` | Enable floating label design |\n| `hint` | `string` | - | Helper text below field |\n| `validFeedback` | `string` | - | Success message when valid |\n| `invalidFeedback` | `string` | - | Error message when invalid |\n| `useNgBootstrap` | `boolean` | - | Use ng-bootstrap datepicker |\n| `displayMonths` | `number` | - | Number of months to display (ng-bootstrap) |\n| `navigation` | `'select' \\| 'arrows' \\| 'none'` | - | Navigation style (ng-bootstrap) |\n| `outsideDays` | `'visible' \\| 'collapsed' \\| 'hidden'` | - | How to display outside days (ng-bootstrap) |\n| `showWeekNumbers` | `boolean` | - | Show week numbers (ng-bootstrap) |\n\n---\n\n### Buttons & Actions\n\nAction buttons provide form submission and navigation controls.\n\n#### Submit Button\n\nForm submission button that's automatically disabled when the form is invalid.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4204/#/examples/button\" class=\"example-frame\" title=\"Button Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n type: 'submit',\n key: 'submit',\n label: 'Create Account',\n props: {\n variant: 'primary',\n size: 'lg',\n },\n}\n```\n\nThe submit button automatically:\n\n- Disables when the form is invalid\n- Emits a `SubmitEvent` when clicked\n- Validates all fields before submission\n\n**Props:**\n\n| Prop | Type | Default | Description |\n| --------- | --------------------------------------------------------------------------------------------------------- | ----------- | ---------------------- |\n| `variant` | `'primary' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'warning' \\| 'info' \\| 'light' \\| 'dark' \\| 'link'` | `'primary'` | Bootstrap button color |\n| `outline` | `boolean` | `false` | Use outline variant |\n| `size` | `'sm' \\| 'lg'` | - | Button size |\n| `block` | `boolean` | `false` | Full-width button |\n| `active` | `boolean` | `false` | Active state styling |\n| `type` | `'button' \\| 'submit' \\| 'reset'` | `'submit'` | HTML button type |\n\n#### Navigation Buttons\n\nNavigation buttons for multi-step (paged) forms.\n\n**Basic Usage:**\n\n```typescript\n{\n fields: [\n {\n key: 'step1',\n type: 'page',\n fields: [\n { key: 'name', type: 'input', value: '', label: 'Name', required: true },\n {\n type: 'next',\n key: 'next',\n label: 'Continue',\n props: { variant: 'primary' },\n },\n ],\n },\n {\n key: 'step2',\n type: 'page',\n fields: [\n { key: 'email', type: 'input', value: '', label: 'Email', required: true },\n { type: 'previous', key: 'back', label: 'Back', props: { variant: 'secondary' } },\n { type: 'submit', key: 'submit', label: 'Submit', props: { variant: 'primary' } },\n ],\n },\n ],\n}\n```\n\n**Button Types:**\n\n- **Next Button**: Navigates to the next page. Automatically disabled when current page has validation errors.\n- **Previous Button**: Navigates to the previous page. Always enabled to allow users to go back.\n\n---\n\n## Theming\n\nBootstrap components use standard Bootstrap CSS classes. Customize appearance using Bootstrap's CSS variables or custom themes:\n\n```typescript\n// Field with custom variant\n{\n key: 'submitBtn',\n type: 'submit',\n label: 'Save Changes',\n props: {\n variant: 'success', // Bootstrap color variant\n size: 'lg',\n outline: true,\n },\n}\n```\n\n### Custom Bootstrap Theme\n\nCustomize Bootstrap variables before importing:\n\n```scss name=\"styles.scss\"\n// Override Bootstrap variables\n$primary: #007bff;\n$secondary: #6c757d;\n$success: #28a745;\n$danger: #dc3545;\n\n// Import Bootstrap\n@import 'bootstrap/dist/css/bootstrap.min.css';\n```\n\n### Component CSS Variables\n\nCustomize field spacing and styling with CSS variables:\n\n```scss\n:root {\n --df-bs-field-gap: 0.75rem;\n --df-bs-label-font-weight: 600;\n --df-bs-hint-color: #6c757d;\n --df-bs-error-color: #dc3545;\n}\n```\n\n## Common Props\n\nAll Bootstrap fields support these common properties:\n\n| Prop | Type | Default | Description |\n| ------ | -------------- | ------- | --------------------------------- |\n| `size` | `'sm' \\| 'lg'` | - | Bootstrap size class |\n| `hint` | `string` | - | Helper text displayed below field |\n\n## Bootstrap-Specific Features\n\n### Floating Labels\n\nEnable modern floating label design for input, select, and textarea fields:\n\n```typescript\n{\n key: 'email',\n type: 'input',\n label: 'Email',\n props: {\n floatingLabel: true,\n },\n}\n```\n\n### Grid System Integration\n\nUse the `col` property for responsive grid layouts:\n\n```typescript\n{\n key: 'nameRow',\n type: 'row',\n fields: [\n {\n key: 'firstName',\n type: 'input',\n label: 'First Name',\n value: '',\n col: 6, // 50% width\n },\n {\n key: 'lastName',\n type: 'input',\n label: 'Last Name',\n value: '',\n col: 6, // 50% width\n },\n ],\n}\n```\n\nOr use `className` for custom Bootstrap grid classes:\n\n```typescript\n{\n key: 'firstName',\n type: 'input',\n label: 'First Name',\n className: 'col-md-6 col-lg-4',\n}\n```\n\n### Validation States\n\nBootstrap validation classes (`.is-invalid`, `.is-valid`) are automatically applied based on form state:\n\n- Fields show errors only after being touched\n- Valid fields can show success feedback with `validFeedback` prop\n- Error messages automatically display below invalid fields\n\n### Switch Styling\n\nCheckboxes and multi-checkboxes can render as Bootstrap switches:\n\n```typescript\n{\n key: 'notifications',\n type: 'checkbox',\n label: 'Enable notifications',\n props: {\n switch: true,\n },\n}\n```\n\n### Button Groups\n\nRadio buttons can render as Bootstrap button groups:\n\n```typescript\n{\n key: 'size',\n type: 'radio',\n label: 'Size',\n options: [\n { value: 's', label: 'S' },\n { value: 'm', label: 'M' },\n { value: 'l', label: 'L' },\n ],\n props: {\n buttonGroup: true,\n buttonSize: 'lg',\n },\n}\n```\n\n## Accessibility\n\nAll Bootstrap components include:\n\n- Proper semantic HTML5 elements\n- ARIA attributes for screen readers\n- Keyboard navigation support\n- Focus management\n- Associated labels and descriptions\n- Error announcements via `aria-describedby`\n\nBootstrap's native HTML elements provide excellent baseline accessibility that's enhanced by ng-forge's form integration.\n\n## Next Steps\n\n- Check out [Examples & Patterns](../../../examples/) for real-world use cases\n- Learn about [Validation](../../validation/basics/) for form validation\n- See [Type Safety](../../advanced/type-safety/basics/) for TypeScript integration\n- Explore [Conditional Logic](../../dynamic-behavior/conditional-logic/overview/) for dynamic field behavior"
231
- },
232
- {
233
- id: "ionic",
234
- title: "Ionic",
235
- category: "ui-libs-integrations",
236
- content: "Mobile-first Ionic field components for ng-forge dynamic forms, optimized for iOS and Android with native platform styling.\n\n------------- | --------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------ |\n| `fill` | `'solid' \\| 'outline'` | `'solid'` | Input fill style |\n| `shape` | `'round'` | - | Rounded shape for inputs |\n| `labelPlacement` | `'start' \\| 'end' \\| 'fixed' \\| 'stacked' \\| 'floating'` | `'start'` | Label position |\n| `color` | `'primary' \\| 'secondary' \\| 'tertiary' \\| 'success' \\| 'warning' \\| 'danger' \\| 'light' \\| 'medium' \\| 'dark'` | `'primary'` | Ionic color theme |\n| `size` | `'small' \\| 'default' \\| 'large'` | `'default'` | Button size |\n| `expand` | `'full' \\| 'block'` | - | Button expand behavior |\n| `buttonFill` | `'clear' \\| 'outline' \\| 'solid' \\| 'default'` | `'solid'` | Button fill style |\n| `strong` | `boolean` | `false` | Bold button text |\n\n---\n\n## Type Augmentation\n\nImporting this package automatically extends `@ng-forge/dynamic-forms` with Ionic-specific field types via TypeScript module augmentation. If you need type safety in a file without importing specific exports, use a bare import:\n\n```typescript\nimport '@ng-forge/dynamic-forms-ionic';\n```\n\n---\n\n## Complete Form Example\n\nHere's a full registration form showcasing multiple Ionic field types optimized for mobile:\n\n<iframe src=\"http://localhost:4203/#/examples/complete-form\" class=\"example-frame\" title=\"Complete Form Demo\"></iframe>\n\nThis example demonstrates:\n\n- Text inputs with mobile keyboard optimization\n- Select dropdowns with native action sheets\n- Checkboxes and toggles with Ionic styling\n- Radio buttons\n- Date/time pickers with native selectors\n- Range sliders with pin display\n- Multi-checkbox selections\n- Form submission\n\n---\n\n## Field Types Reference\n\nComplete reference for all Ionic field types with mobile-first design, native platform styling, and comprehensive validation.\n\n### Text Input Fields\n\nText input fields provide mobile-optimized text entry with platform-specific keyboards.\n\n#### Input\n\nText input field with mobile keyboard optimization and platform-adaptive styling.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4203/#/examples/input\" class=\"example-frame\" title=\"Input Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'email',\n type: 'input',\n label: 'Email Address',\n value: '',\n required: true,\n email: true,\n props: {\n type: 'email',\n placeholder: 'your@email.com',\n clearInput: true,\n },\n}\n```\n\n**Field Properties:**\n\n| Property | Type | Description |\n| ------------- | ------------------ | ---------------------------------- |\n| `key` | `string` | Unique field identifier (required) |\n| `type` | `'input'` | Field type (required) |\n| `value` | `string \\| number` | Initial value |\n| `label` | `string` | Field label |\n| `placeholder` | `string` | Placeholder text |\n| `required` | `boolean` | Mark field as required |\n| `disabled` | `boolean` | Disable the field |\n| `readonly` | `boolean` | Make field read-only |\n\n**Validation Properties:**\n\n| Property | Type | Description |\n| ----------- | ------------------ | --------------------------------- |\n| `email` | `boolean` | Email format validation |\n| `minLength` | `number` | Minimum character length |\n| `maxLength` | `number` | Maximum character length |\n| `min` | `number` | Minimum value (for number inputs) |\n| `max` | `number` | Maximum value (for number inputs) |\n| `pattern` | `string \\| RegExp` | RegEx pattern validation |\n\n**Props (Ionic-Specific):**\n\n| Prop | Type | Default | Description |\n| ---------------- | ------------------------------------------------------------------------------ | --------- | ----------------------- |\n| `type` | `'text' \\| 'email' \\| 'password' \\| 'number' \\| 'tel' \\| 'url'` | `'text'` | HTML input type |\n| `placeholder` | `string` | - | Placeholder text |\n| `clearInput` | `boolean` | `false` | Show clear button |\n| `fill` | `'solid' \\| 'outline'` | `'solid'` | Input fill style |\n| `shape` | `'round'` | - | Rounded input shape |\n| `labelPlacement` | `'start' \\| 'end' \\| 'fixed' \\| 'stacked' \\| 'floating'` | `'start'` | Position of the label |\n| `color` | `'primary' \\| 'secondary' \\| 'tertiary' \\| 'success' \\| 'warning' \\| 'danger'` | - | Ionic theme color |\n| `helperText` | `string` | - | Helper text below input |\n| `counter` | `boolean` | `false` | Show character counter |\n\n#### Textarea\n\nMulti-line text input field with auto-grow support for mobile devices.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4203/#/examples/textarea\" class=\"example-frame\" title=\"Textarea Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'bio',\n type: 'textarea',\n value: '',\n label: 'Biography',\n placeholder: 'Tell us about yourself',\n required: true,\n maxLength: 500,\n props: {\n rows: 4,\n autoGrow: true,\n },\n}\n```\n\n**Props (Ionic-Specific):**\n\n| Prop | Type | Default | Description |\n| ---------------- | ------------------------------------------------------------------------------ | --------- | ---------------------------- |\n| `rows` | `number` | `4` | Number of visible rows |\n| `autoGrow` | `boolean` | `false` | Auto-resize based on content |\n| `placeholder` | `string` | - | Placeholder text |\n| `fill` | `'solid' \\| 'outline'` | `'solid'` | Textarea fill style |\n| `labelPlacement` | `'start' \\| 'end' \\| 'fixed' \\| 'stacked' \\| 'floating'` | `'start'` | Position of the label |\n| `color` | `'primary' \\| 'secondary' \\| 'tertiary' \\| 'success' \\| 'warning' \\| 'danger'` | - | Ionic theme color |\n| `helperText` | `string` | - | Helper text below field |\n| `counter` | `boolean` | `false` | Show character counter |\n\n---\n\n### Selection Fields\n\nSelection fields enable users to choose from predefined options with mobile-optimized interfaces.\n\n#### Select\n\nDropdown selection field with native action sheets on mobile devices.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4203/#/examples/select\" class=\"example-frame\" title=\"Select Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'country',\n type: 'select',\n value: '',\n label: 'Country',\n required: true,\n options: [\n { value: 'us', label: 'United States' },\n { value: 'uk', label: 'United Kingdom' },\n { value: 'ca', label: 'Canada' },\n ],\n props: {\n interface: 'action-sheet',\n },\n}\n```\n\n**Props (Ionic-Specific):**\n\n| Prop | Type | Default | Description |\n| ---------------- | ------------------------------------------------------------------------------ | ---------------- | --------------------- |\n| `interface` | `'action-sheet' \\| 'alert' \\| 'popover'` | `'action-sheet'` | Mobile selection UI |\n| `multiple` | `boolean` | `false` | Enable multi-select |\n| `placeholder` | `string` | - | Placeholder text |\n| `cancelText` | `string` | `'Cancel'` | Cancel button text |\n| `okText` | `string` | `'OK'` | OK button text |\n| `fill` | `'solid' \\| 'outline'` | `'solid'` | Select fill style |\n| `labelPlacement` | `'start' \\| 'end' \\| 'fixed' \\| 'stacked' \\| 'floating'` | `'start'` | Position of the label |\n| `color` | `'primary' \\| 'secondary' \\| 'tertiary' \\| 'success' \\| 'warning' \\| 'danger'` | - | Ionic theme color |\n\n#### Radio\n\nRadio button group for selecting a single option with mobile-friendly spacing.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4203/#/examples/radio\" class=\"example-frame\" title=\"Radio Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'size',\n type: 'radio',\n value: '',\n label: 'Select Size',\n required: true,\n options: [\n { value: 'small', label: 'Small' },\n { value: 'medium', label: 'Medium' },\n { value: 'large', label: 'Large' },\n ],\n}\n```\n\n**Props (Ionic-Specific):**\n\n| Prop | Type | Default | Description |\n| ---------------- | ------------------------------------------------------------------------------ | ------- | ------------------------- |\n| `labelPlacement` | `'start' \\| 'end' \\| 'fixed' \\| 'stacked'` | `'end'` | Position of option labels |\n| `justify` | `'start' \\| 'end' \\| 'space-between'` | - | Layout justification |\n| `color` | `'primary' \\| 'secondary' \\| 'tertiary' \\| 'success' \\| 'warning' \\| 'danger'` | - | Ionic theme color |\n\n#### Checkbox\n\nBoolean checkbox control optimized for touch interaction.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4203/#/examples/checkbox\" class=\"example-frame\" title=\"Checkbox Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'terms',\n type: 'checkbox',\n value: false,\n label: 'I accept the terms and conditions',\n required: true,\n}\n```\n\n**Props (Ionic-Specific):**\n\n| Prop | Type | Default | Description |\n| ---------------- | ------------------------------------------------------------------------------ | ------- | ---------------------- |\n| `labelPlacement` | `'start' \\| 'end' \\| 'fixed' \\| 'stacked'` | `'end'` | Position of label text |\n| `justify` | `'start' \\| 'end' \\| 'space-between'` | - | Layout justification |\n| `color` | `'primary' \\| 'secondary' \\| 'tertiary' \\| 'success' \\| 'warning' \\| 'danger'` | - | Ionic theme color |\n\n#### Multi-Checkbox\n\nMultiple checkbox selection field for choosing multiple options.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4203/#/examples/multi-checkbox\" class=\"example-frame\" title=\"Multi-Checkbox Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'interests',\n type: 'multi-checkbox',\n value: [],\n label: 'Select Your Interests',\n required: true,\n options: [\n { value: 'sports', label: 'Sports' },\n { value: 'music', label: 'Music' },\n { value: 'reading', label: 'Reading' },\n { value: 'travel', label: 'Travel' },\n ],\n}\n```\n\n**Props (Ionic-Specific):**\n\n| Prop | Type | Default | Description |\n| ---------------- | ------------------------------------------------------------------------------ | ------- | -------------------- |\n| `labelPlacement` | `'start' \\| 'end' \\| 'fixed' \\| 'stacked'` | `'end'` | Position of labels |\n| `justify` | `'start' \\| 'end' \\| 'space-between'` | - | Layout justification |\n| `color` | `'primary' \\| 'secondary' \\| 'tertiary' \\| 'success' \\| 'warning' \\| 'danger'` | - | Ionic theme color |\n\n---\n\n### Interactive Fields\n\nInteractive fields provide advanced user input controls optimized for mobile interaction.\n\n#### Toggle\n\nSlide toggle switch for boolean on/off selections with smooth animations.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4203/#/examples/toggle\" class=\"example-frame\" title=\"Toggle Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'notifications',\n type: 'toggle',\n value: false,\n label: 'Enable notifications',\n}\n```\n\n**Props (Ionic-Specific):**\n\n| Prop | Type | Default | Description |\n| ------------------- | ------------------------------------------------------------------------------ | ------- | ---------------------- |\n| `labelPlacement` | `'start' \\| 'end' \\| 'fixed' \\| 'stacked'` | `'end'` | Position of label text |\n| `justify` | `'start' \\| 'end' \\| 'space-between'` | - | Layout justification |\n| `enableOnOffLabels` | `boolean` | `false` | Show on/off labels |\n| `color` | `'primary' \\| 'secondary' \\| 'tertiary' \\| 'success' \\| 'warning' \\| 'danger'` | - | Ionic theme color |\n\n#### Slider\n\nRange slider control with pin display and snap-to-tick support.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4203/#/examples/slider\" class=\"example-frame\" title=\"Slider Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'volume',\n type: 'slider',\n value: 50,\n label: 'Volume',\n minValue: 0,\n maxValue: 100,\n step: 1,\n props: {\n pin: true,\n ticks: true,\n },\n}\n```\n\n**Field Properties:**\n\n| Property | Type | Default | Description |\n| ---------- | -------- | ------- | -------------------- |\n| `minValue` | `number` | `0` | Minimum slider value |\n| `maxValue` | `number` | `100` | Maximum slider value |\n| `step` | `number` | `1` | Value increment step |\n\n**Props (Ionic-Specific):**\n\n| Prop | Type | Default | Description |\n| ---------------- | ------------------------------------------------------------------------------ | --------- | ------------------------------ |\n| `pin` | `boolean` | `false` | Show value pin above knob |\n| `pinFormatter` | `(value: number) => string \\| number` | - | Custom formatter for pin value |\n| `ticks` | `boolean` | `false` | Show tick marks |\n| `snaps` | `boolean` | `false` | Snap to tick marks |\n| `dualKnobs` | `boolean` | `false` | Enable dual knob mode |\n| `color` | `'primary' \\| 'secondary' \\| 'tertiary' \\| 'success' \\| 'warning' \\| 'danger'` | - | Ionic theme color |\n| `labelPlacement` | `'start' \\| 'end' \\| 'fixed' \\| 'stacked'` | `'start'` | Position of label |\n\n#### Datepicker\n\nDate and time selection with native mobile pickers for iOS and Android.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4203/#/examples/datepicker\" class=\"example-frame\" title=\"Datepicker Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'birthdate',\n type: 'datepicker',\n value: null,\n label: 'Date of Birth',\n required: true,\n props: {\n presentation: 'date',\n },\n}\n```\n\n**Field Properties:**\n\n| Property | Type | Description |\n| --------- | ------------------------ | ----------------------- |\n| `minDate` | `Date \\| string \\| null` | Minimum selectable date |\n| `maxDate` | `Date \\| string \\| null` | Maximum selectable date |\n\n**Props (Ionic-Specific):**\n\n| Prop | Type | Default | Description |\n| ---------------------- | ------------------------------------------------------------------------------------- | ---------- | ------------------------ |\n| `presentation` | `'date' \\| 'time' \\| 'date-time' \\| 'time-date' \\| 'month' \\| 'month-year' \\| 'year'` | `'date'` | Picker type |\n| `showDefaultButtons` | `boolean` | `true` | Show cancel/done buttons |\n| `showDefaultTitle` | `boolean` | - | Show default title |\n| `showDefaultTimeLabel` | `boolean` | - | Show default time label |\n| `showClearButton` | `boolean` | - | Show clear button |\n| `cancelText` | `string` | `'Cancel'` | Cancel button text |\n| `doneText` | `string` | `'Done'` | Done button text |\n| `multiple` | `boolean` | `false` | Allow multiple selection |\n| `preferWheel` | `boolean` | `false` | Use wheel picker style |\n| `size` | `'cover' \\| 'fixed'` | - | Picker overlay size |\n| `color` | `'primary' \\| 'secondary' \\| 'tertiary' \\| 'success' \\| 'warning' \\| 'danger'` | - | Ionic theme color |\n\n---\n\n### Buttons & Actions\n\nAction buttons provide form submission and navigation controls with Ionic styling.\n\n#### Submit Button\n\nForm submission button that's automatically disabled when the form is invalid.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4203/#/examples/button\" class=\"example-frame\" title=\"Button Field Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n type: 'submit',\n key: 'submit',\n label: 'Create Account',\n props: {\n color: 'primary',\n expand: 'block',\n },\n}\n```\n\nThe submit button automatically:\n\n- Disables when the form is invalid\n- Emits a `SubmitEvent` when clicked\n- Validates all fields before submission\n\n**Props:**\n\n| Prop | Type | Default | Description |\n| -------- | --------------------------------------------------------------------------------------------------------------- | ----------- | ----------------- |\n| `expand` | `'full' \\| 'block'` | - | Button width |\n| `fill` | `'clear' \\| 'outline' \\| 'solid' \\| 'default'` | `'solid'` | Button fill style |\n| `shape` | `'round'` | - | Rounded button |\n| `size` | `'small' \\| 'default' \\| 'large'` | - | Button size |\n| `color` | `'primary' \\| 'secondary' \\| 'tertiary' \\| 'success' \\| 'warning' \\| 'danger' \\| 'light' \\| 'medium' \\| 'dark'` | `'primary'` | Ionic theme color |\n| `strong` | `boolean` | `false` | Use strong font |\n\n#### Navigation Buttons\n\nNavigation buttons for multi-step (paged) forms with platform-adaptive styling.\n\n**Basic Usage:**\n\n```typescript\n{\n fields: [\n {\n key: 'step1',\n type: 'page',\n fields: [\n { key: 'step1Title', type: 'text', label: 'Step 1', props: { elementType: 'h3' } },\n { key: 'name', type: 'input', value: '', label: 'Name', required: true },\n {\n type: 'next',\n key: 'next',\n label: 'Continue',\n props: { color: 'primary', expand: 'block' },\n },\n ],\n },\n {\n key: 'step2',\n type: 'page',\n fields: [\n { key: 'step2Title', type: 'text', label: 'Step 2', props: { elementType: 'h3' } },\n { key: 'email', type: 'input', value: '', label: 'Email', required: true },\n { type: 'previous', key: 'back', label: 'Back' },\n { type: 'submit', key: 'submit', label: 'Submit', props: { color: 'primary' } },\n ],\n },\n ],\n}\n```\n\n**Button Types:**\n\n- **Next Button**: Navigates to the next page. Automatically disabled when current page has validation errors.\n- **Previous Button**: Navigates to the previous page. Always enabled to allow users to go back.\n\n---\n\n## Theming\n\nIonic components use CSS variables for theming. Customize colors and styles globally or per-component.\n\n### Color Palette\n\nIonic provides a comprehensive color system:\n\n```typescript\n// Field with custom color\n{\n key: 'email',\n type: 'input',\n label: 'Email',\n props: {\n color: 'success', // Uses your theme's success color\n },\n}\n```\n\n**Available Colors:**\n\n- `'primary'` - Primary brand color\n- `'secondary'` - Secondary brand color\n- `'tertiary'` - Tertiary brand color\n- `'success'` - Success/positive actions\n- `'warning'` - Warning states\n- `'danger'` - Error/destructive actions\n- `'light'` - Light variant\n- `'medium'` - Medium gray\n- `'dark'` - Dark variant\n\n### Custom Theme Variables\n\nOverride Ionic's default colors in your global styles:\n\n```scss name=\"styles.scss\"\n:root {\n --ion-color-primary: #3880ff;\n --ion-color-primary-rgb: 56, 128, 255;\n --ion-color-primary-contrast: #ffffff;\n --ion-color-primary-contrast-rgb: 255, 255, 255;\n --ion-color-primary-shade: #3171e0;\n --ion-color-primary-tint: #4c8dff;\n\n // Customize other colors...\n}\n```\n\nSee [Ionic Theming Guide](https://ionicframework.com/docs/theming/basics) for complete customization options.\n\n## Common Props\n\nAll Ionic fields support these common properties:\n\n| Prop | Type | Default | Description |\n| ------- | ------------------------------------------------------------------------------ | ----------- | ------------------------------- |\n| `color` | `'primary' \\| 'secondary' \\| 'tertiary' \\| 'success' \\| 'warning' \\| 'danger'` | `'primary'` | Ionic theme color |\n| `mode` | `'ios' \\| 'md'` | - | Force platform-specific styling |\n\n## Mobile Optimization\n\nAll Ionic components are designed for mobile devices:\n\n### Touch Targets\n\n- **Minimum size**: 44x44px touch areas for accessibility\n- **Spacing**: Adequate padding for comfortable tapping\n- **Visual feedback**: Ripple effects and state changes\n\n### Native Keyboards\n\nInput fields automatically show the appropriate keyboard:\n\n```typescript\n{\n key: 'email',\n type: 'input',\n props: {\n type: 'email', // Shows email keyboard\n inputmode: 'email', // Forces email input mode\n },\n}\n```\n\n**Inputmode Options:**\n\n- `'text'` - Standard keyboard\n- `'decimal'` - Numeric with decimal\n- `'numeric'` - Numeric only\n- `'tel'` - Telephone keypad\n- `'search'` - Search keyboard\n- `'email'` - Email keyboard\n- `'url'` - URL keyboard\n\n### Platform Styling\n\nIonic automatically adapts to the platform:\n\n- **iOS**: Native iOS styling with SF Pro font and iOS design patterns\n- **Android**: Material Design with Roboto font and MD patterns\n- **Automatic detection**: Based on device platform\n- **Manual override**: Use `mode` prop to force specific styling\n\n```typescript\n{\n key: 'name',\n type: 'input',\n label: 'Name',\n props: {\n mode: 'ios', // Force iOS styling on all platforms\n },\n}\n```\n\n### Performance\n\n- **Hardware acceleration**: GPU-accelerated animations\n- **Virtual scrolling**: Efficient rendering for long lists\n- **Lazy loading**: Components load on demand\n- **Gesture support**: Native swipe and touch gestures\n\n## Accessibility\n\nAll Ionic components include comprehensive accessibility features:\n\n### ARIA Support\n\n- Proper ARIA roles and attributes\n- ARIA labels for screen readers\n- ARIA-describedby for error messages\n- ARIA-invalid for validation states\n\n### Keyboard Navigation\n\n- Full keyboard support for all interactive elements\n- Tab navigation with proper focus order\n- Enter/Space for button activation\n- Arrow keys for option selection\n\n### Screen Readers\n\n- Descriptive labels for all form controls\n- Error message announcements\n- State change notifications\n- Context-aware instructions\n\n### Focus Management\n\n- Visible focus indicators\n- Logical focus order\n- Focus trap for modals\n- Auto-focus on error fields\n\n### High Contrast\n\n- Compatible with high contrast modes\n- Sufficient color contrast ratios (WCAG AA)\n- Clear visual distinctions\n- Icon and text alternatives\n\n## Next Steps\n\n- Check out [Examples & Patterns](../../../examples/) for real-world use cases\n- Learn about [Validation](../../validation/basics/) for form validation\n- See [Type Safety](../../advanced/type-safety/basics/) for TypeScript integration\n- Explore [Conditional Logic](../../dynamic-behavior/conditional-logic/overview/) for dynamic field behavior"
237
- },
238
- {
239
- id: "material",
240
- title: "Material",
241
- category: "ui-libs-integrations",
242
- content: "Beautiful Material Design field components for ng-forge dynamic forms, built with Angular Material.\n\n-------------- | --------------------------------- | ----------- | ----------------------------- |\n| `appearance` | `'fill' \\| 'outline'` | `'fill'` | Form field visual style |\n| `subscriptSizing` | `'fixed' \\| 'dynamic'` | `'fixed'` | Error/hint spacing behavior |\n| `color` | `'primary' \\| 'accent' \\| 'warn'` | `'primary'` | Theme color for controls |\n| `labelPosition` | `'before' \\| 'after'` | `'after'` | Label position for checkboxes |\n| `disableRipple` | `boolean` | `false` | Disable ripple effect |\n\n---\n\n## Type Augmentation\n\nImporting this package automatically extends `@ng-forge/dynamic-forms` with Material-specific field types via TypeScript module augmentation. If you need type safety in a file without importing specific exports, use a bare import:\n\n```typescript\nimport '@ng-forge/dynamic-forms-material';\n```\n\n---\n\n## Complete Form Example\n\nHere's a full registration form showcasing multiple Material Design field types:\n\n<iframe src=\"http://localhost:4201/#/examples/complete-form\" class=\"example-frame\" title=\"Complete Form Demo\"></iframe>\n\nThis example demonstrates:\n\n- Text inputs with validation\n- Select dropdowns\n- Checkboxes and toggles\n- Radio buttons\n- Date pickers\n- Sliders\n- Multi-checkbox selections\n- Form submission\n\n---\n\n## Field Types Reference\n\nComplete reference for all Material Design field types with comprehensive validation, accessibility features, and Material Design styling.\n\n### Text Input Fields\n\nText input fields provide user-friendly text entry with Material Design styling.\n\n#### Input\n\nText input field with HTML5 type support.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4201/#/examples/input\" class=\"example-frame\" title=\"Input Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'email',\n type: 'input',\n label: 'Email Address',\n value: '',\n required: true,\n email: true,\n props: {\n type: 'email',\n appearance: 'outline',\n placeholder: 'Enter your email',\n },\n}\n```\n\n**Field Properties:**\n\n| Property | Type | Description |\n| ------------- | ------------------ | ---------------------------------- |\n| `key` | `string` | Unique field identifier (required) |\n| `type` | `'input'` | Field type (required) |\n| `value` | `string \\| number` | Initial value |\n| `label` | `string` | Field label |\n| `placeholder` | `string` | Placeholder text |\n| `required` | `boolean` | Mark field as required |\n| `disabled` | `boolean` | Disable the field |\n| `readonly` | `boolean` | Make field read-only |\n\n**Validation Properties:**\n\n| Property | Type | Description |\n| ----------- | ------------------ | --------------------------------- |\n| `email` | `boolean` | Email format validation |\n| `minLength` | `number` | Minimum character length |\n| `maxLength` | `number` | Maximum character length |\n| `min` | `number` | Minimum value (for number inputs) |\n| `max` | `number` | Maximum value (for number inputs) |\n| `pattern` | `string \\| RegExp` | RegEx pattern validation |\n\n**Props (Material-Specific):**\n\n| Prop | Type | Default | Description |\n| ----------------- | --------------------------------------------------------------- | --------- | ------------------------- |\n| `type` | `'text' \\| 'email' \\| 'password' \\| 'number' \\| 'tel' \\| 'url'` | `'text'` | HTML input type |\n| `appearance` | `'fill' \\| 'outline'` | `'fill'` | Material form field style |\n| `hint` | `string` | - | Helper text below input |\n| `subscriptSizing` | `'fixed' \\| 'dynamic'` | `'fixed'` | Error/hint spacing |\n\n#### Textarea\n\nMulti-line text input field with Material Design styling.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4201/#/examples/textarea\" class=\"example-frame\" title=\"Textarea Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'bio',\n type: 'textarea',\n value: '',\n label: 'Biography',\n placeholder: 'Tell us about yourself',\n required: true,\n}\n```\n\n**Props (Material-Specific):**\n\n| Prop | Type | Default | Description |\n| ----------------- | ------------------------------------------------ | ------------ | ------------------------- |\n| `appearance` | `'fill' \\| 'outline'` | `'outline'` | Visual style |\n| `rows` | `number` | `4` | Number of visible rows |\n| `cols` | `number` | - | Number of visible columns |\n| `resize` | `'none' \\| 'both' \\| 'horizontal' \\| 'vertical'` | `'vertical'` | Resize behavior |\n| `subscriptSizing` | `'fixed' \\| 'dynamic'` | `'fixed'` | Error/hint sizing |\n| `hint` | `string` | - | Help text below field |\n\n---\n\n### Selection Fields\n\nSelection fields enable users to choose from predefined options.\n\n#### Select\n\nDropdown selection field. Supports both single and multi-select modes.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4201/#/examples/select\" class=\"example-frame\" title=\"Select Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'country',\n type: 'select',\n value: '',\n label: 'Country',\n required: true,\n options: [\n { value: 'us', label: 'United States' },\n { value: 'uk', label: 'United Kingdom' },\n { value: 'ca', label: 'Canada' },\n ],\n}\n```\n\n**Props (Material-Specific):**\n\n| Prop | Type | Default | Description |\n| ----------------- | ---------------------- | ----------- | --------------------- |\n| `appearance` | `'fill' \\| 'outline'` | `'outline'` | Visual style |\n| `multiple` | `boolean` | `false` | Enable multi-select |\n| `hint` | `string` | - | Help text below field |\n| `subscriptSizing` | `'fixed' \\| 'dynamic'` | `'fixed'` | Error/hint sizing |\n\n#### Radio\n\nRadio button group for selecting a single option.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4201/#/examples/radio\" class=\"example-frame\" title=\"Radio Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'size',\n type: 'radio',\n value: '',\n label: 'Select Size',\n required: true,\n options: [\n { value: 'small', label: 'Small' },\n { value: 'medium', label: 'Medium' },\n { value: 'large', label: 'Large' },\n ],\n}\n```\n\n**Props (Material-Specific):**\n\n| Prop | Type | Default | Description |\n| --------------- | --------------------------------- | ----------- | ------------------------- |\n| `color` | `'primary' \\| 'accent' \\| 'warn'` | `'primary'` | Material theme color |\n| `labelPosition` | `'before' \\| 'after'` | `'after'` | Position of option labels |\n\n#### Checkbox\n\nBoolean checkbox control for single true/false selections.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4201/#/examples/checkbox\" class=\"example-frame\" title=\"Checkbox Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'terms',\n type: 'checkbox',\n value: false,\n label: 'I accept the terms and conditions',\n required: true,\n}\n```\n\n**Props (Material-Specific):**\n\n| Prop | Type | Default | Description |\n| --------------- | --------------------------------- | ----------- | ---------------------- |\n| `color` | `'primary' \\| 'accent' \\| 'warn'` | `'primary'` | Material theme color |\n| `labelPosition` | `'before' \\| 'after'` | `'after'` | Position of label text |\n\n#### Multi-Checkbox\n\nMultiple checkbox selection field for choosing multiple options.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4201/#/examples/multi-checkbox\" class=\"example-frame\" title=\"Multi-Checkbox Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'interests',\n type: 'multi-checkbox',\n value: [],\n label: 'Select Your Interests',\n required: true,\n options: [\n { value: 'sports', label: 'Sports' },\n { value: 'music', label: 'Music' },\n { value: 'reading', label: 'Reading' },\n { value: 'travel', label: 'Travel' },\n ],\n}\n```\n\n---\n\n### Interactive Fields\n\nInteractive fields provide advanced user input controls.\n\n#### Toggle\n\nSlide toggle switch for boolean on/off selections.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4201/#/examples/toggle\" class=\"example-frame\" title=\"Toggle Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'notifications',\n type: 'toggle',\n value: false,\n label: 'Enable notifications',\n}\n```\n\n**Props (Material-Specific):**\n\n| Prop | Type | Default | Description |\n| --------------- | --------------------------------- | ----------- | ---------------------- |\n| `color` | `'primary' \\| 'accent' \\| 'warn'` | `'primary'` | Material theme color |\n| `labelPosition` | `'before' \\| 'after'` | `'after'` | Position of label text |\n\n#### Slider\n\nNumeric slider control for selecting values from a range.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4201/#/examples/slider\" class=\"example-frame\" title=\"Slider Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'volume',\n type: 'slider',\n value: 50,\n label: 'Volume',\n minValue: 0,\n maxValue: 100,\n step: 1,\n}\n```\n\n**Props (Material-Specific):**\n\n| Prop | Type | Default | Description |\n| ------------------------------- | --------------------------------- | ----------- | ---------------------------- |\n| `thumbLabel` / `showThumbLabel` | `boolean` | `false` | Show value tooltip on thumb |\n| `tickInterval` | `number \\| 'auto'` | - | Show tick marks at intervals |\n| `color` | `'primary' \\| 'accent' \\| 'warn'` | `'primary'` | Material theme color |\n| `hint` | `string` | - | Help text below slider |\n\n#### Datepicker\n\nDate selection field with Material Design calendar popup.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4201/#/examples/datepicker\" class=\"example-frame\" title=\"Datepicker Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'birthdate',\n type: 'datepicker',\n value: null,\n label: 'Date of Birth',\n required: true,\n}\n```\n\n**Props (Material-Specific):**\n\n| Prop | Type | Default | Description |\n| ----------------- | ----------------------------------- | ----------- | --------------------------- |\n| `appearance` | `'fill' \\| 'outline'` | `'outline'` | Visual style |\n| `color` | `'primary' \\| 'accent' \\| 'warn'` | `'primary'` | Material theme color |\n| `startView` | `'month' \\| 'year' \\| 'multi-year'` | `'month'` | Initial calendar view |\n| `touchUi` | `boolean` | `false` | Touch-optimized calendar UI |\n| `subscriptSizing` | `'fixed' \\| 'dynamic'` | `'fixed'` | Error/hint sizing |\n| `hint` | `string` | - | Help text below field |\n\n---\n\n### Buttons & Actions\n\nAction buttons provide form submission and navigation controls.\n\n#### Submit Button\n\nForm submission button that's automatically disabled when the form is invalid.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4201/#/examples/button\" class=\"example-frame\" title=\"Button Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n type: 'submit',\n key: 'submit',\n label: 'Create Account',\n props: {\n color: 'primary',\n },\n}\n```\n\nThe submit button automatically:\n\n- Disables when the form is invalid\n- Emits a `SubmitEvent` when clicked\n- Validates all fields before submission\n\n**Props:**\n\n| Prop | Type | Default | Description |\n| ------- | --------------------------------- | ----------- | -------------------- |\n| `color` | `'primary' \\| 'accent' \\| 'warn'` | `'primary'` | Material theme color |\n\n#### Navigation Buttons\n\nNavigation buttons for multi-step (paged) forms.\n\n**Basic Usage:**\n\n```typescript\n{\n fields: [\n {\n key: 'step1',\n type: 'page',\n fields: [\n { key: 'step1Title', type: 'text', label: 'Step 1', props: { elementType: 'h3' } },\n { key: 'name', type: 'input', value: '', label: 'Name', required: true },\n {\n type: 'next',\n key: 'next',\n label: 'Continue',\n props: { color: 'primary' },\n },\n ],\n },\n {\n key: 'step2',\n type: 'page',\n fields: [\n { key: 'step2Title', type: 'text', label: 'Step 2', props: { elementType: 'h3' } },\n { key: 'email', type: 'input', value: '', label: 'Email', required: true },\n { type: 'previous', key: 'back', label: 'Back' },\n { type: 'submit', key: 'submit', label: 'Submit', props: { color: 'primary' } },\n ],\n },\n ],\n}\n```\n\n**Button Types:**\n\n- **Next Button**: Navigates to the next page. Automatically disabled when current page has validation errors.\n- **Previous Button**: Navigates to the previous page. Always enabled to allow users to go back.\n\n---\n\n## Theming\n\nMaterial components automatically inherit your Angular Material theme. Customize colors using Material's theming system:\n\n```typescript\n// Field with custom color\n{\n key: 'agreeToTerms',\n type: 'checkbox',\n label: 'I agree to the terms and conditions',\n props: {\n color: 'accent', // 'primary' | 'accent' | 'warn'\n },\n}\n```\n\n## Common Props\n\nAll Material fields support these common properties:\n\n| Prop | Type | Default | Description |\n| ----------------- | --------------------------------- | ----------- | --------------------------------- |\n| `appearance` | `'fill' \\| 'outline'` | `'fill'` | Form field appearance style |\n| `color` | `'primary' \\| 'accent' \\| 'warn'` | `'primary'` | Theme color |\n| `hint` | `string` | - | Helper text displayed below field |\n| `subscriptSizing` | `'fixed' \\| 'dynamic'` | `'fixed'` | Error/hint spacing behavior |\n\n## Accessibility\n\nAll Material Design components include:\n\n- Proper ARIA attributes\n- Keyboard navigation support\n- Screen reader compatibility\n- Focus management\n- Error announcements\n\n## Next Steps\n\n- Check out [Examples & Patterns](../../../examples/) for real-world use cases\n- Learn about [Validation](../../validation/basics/) for form validation\n- See [Type Safety](../../advanced/type-safety/basics/) for TypeScript integration\n- Explore [Conditional Logic](../../dynamic-behavior/conditional-logic/overview/) for dynamic field behavior"
243
- },
244
- {
245
- id: "primeng",
246
- title: "Primeng",
247
- category: "ui-libs-integrations",
248
- content: "Beautiful PrimeNG field components for ng-forge dynamic forms, built with the PrimeNG design system.\n\n------- | ----------------------------------------------------------------------------------------------- | ------------ | ---------------------- |\n| `size` | `'small' \\| 'large'` | - | Component size |\n| `variant` | `'outlined' \\| 'filled'` | `'outlined'` | Input visual style |\n| `severity` | `'primary' \\| 'secondary' \\| 'success' \\| 'info' \\| 'warn' \\| 'danger' \\| 'help' \\| 'contrast'` | `'primary'` | Button color |\n| `text` | `boolean` | `false` | Text-only button style |\n| `outlined` | `boolean` | `false` | Outlined button style |\n| `raised` | `boolean` | `false` | Raised button style |\n| `rounded` | `boolean` | `false` | Rounded button style |\n\n---\n\n## Type Augmentation\n\nImporting this package automatically extends `@ng-forge/dynamic-forms` with PrimeNG-specific field types via TypeScript module augmentation. If you need type safety in a file without importing specific exports, use a bare import:\n\n```typescript\nimport '@ng-forge/dynamic-forms-primeng';\n```\n\n---\n\n## Complete Form Example\n\nHere's a full registration form showcasing multiple PrimeNG field types:\n\n<iframe src=\"http://localhost:4202/#/examples/complete-form\" class=\"example-frame\" title=\"Complete Form Demo\"></iframe>\n\nThis example demonstrates:\n\n- Text inputs with validation\n- Select dropdowns with search\n- Checkboxes and toggles\n- Radio buttons\n- Date pickers\n- Sliders\n- Multi-checkbox selections\n- Form submission\n\n---\n\n## Field Types Reference\n\nComplete reference for all PrimeNG field types with comprehensive validation, accessibility features, and PrimeNG styling.\n\n### Text Input Fields\n\nText input fields provide user-friendly text entry with PrimeNG styling.\n\n#### Input\n\nText input field with HTML5 type support and PrimeNG styling.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4202/#/examples/input\" class=\"example-frame\" title=\"Input Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'email',\n type: 'input',\n value: '',\n label: 'Email Address',\n required: true,\n email: true,\n props: {\n type: 'email',\n variant: 'outlined',\n placeholder: 'Enter your email',\n hint: 'We will never share your email',\n },\n}\n```\n\n**Field Properties:**\n\n| Property | Type | Description |\n| ------------- | ------------------ | ---------------------------------- |\n| `key` | `string` | Unique field identifier (required) |\n| `type` | `'input'` | Field type (required) |\n| `value` | `string \\| number` | Initial value |\n| `label` | `string` | Field label |\n| `placeholder` | `string` | Placeholder text |\n| `required` | `boolean` | Mark field as required |\n| `disabled` | `boolean` | Disable the field |\n| `readonly` | `boolean` | Make field read-only |\n\n**Validation Properties:**\n\n| Property | Type | Description |\n| ----------- | ------------------ | --------------------------------- |\n| `email` | `boolean` | Email format validation |\n| `minLength` | `number` | Minimum character length |\n| `maxLength` | `number` | Maximum character length |\n| `min` | `number` | Minimum value (for number inputs) |\n| `max` | `number` | Maximum value (for number inputs) |\n| `pattern` | `string \\| RegExp` | RegEx pattern validation |\n\n**Props (PrimeNG-Specific):**\n\n| Prop | Type | Default | Description |\n| ------------ | --------------------------------------------------------------- | ------------ | ----------------------- |\n| `type` | `'text' \\| 'email' \\| 'password' \\| 'number' \\| 'tel' \\| 'url'` | `'text'` | HTML input type |\n| `variant` | `'outlined' \\| 'filled'` | `'outlined'` | Visual style |\n| `size` | `'small' \\| 'large'` | - | Input size |\n| `hint` | `string` | - | Helper text below input |\n| `styleClass` | `string` | - | CSS class for input |\n\n#### Textarea\n\nMulti-line text input field with PrimeNG styling and auto-resize support.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4202/#/examples/textarea\" class=\"example-frame\" title=\"Textarea Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'bio',\n type: 'textarea',\n value: '',\n label: 'Biography',\n placeholder: 'Tell us about yourself',\n required: true,\n minLength: 50,\n maxLength: 500,\n props: {\n rows: 6,\n autoResize: true,\n hint: 'Maximum 500 characters',\n },\n}\n```\n\n**Props (PrimeNG-Specific):**\n\n| Prop | Type | Default | Description |\n| ------------ | --------- | ------- | ---------------------------- |\n| `rows` | `number` | `4` | Number of visible rows |\n| `autoResize` | `boolean` | `false` | Auto-resize based on content |\n| `maxlength` | `number` | - | Maximum character limit |\n| `hint` | `string` | - | Help text below field |\n| `styleClass` | `string` | - | CSS class for textarea |\n\n---\n\n### Selection Fields\n\nSelection fields enable users to choose from predefined options.\n\n#### Select\n\nDropdown selection field with search capability and virtual scrolling support.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4202/#/examples/select\" class=\"example-frame\" title=\"Select Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'country',\n type: 'select',\n value: '',\n label: 'Country',\n required: true,\n options: [\n { value: 'us', label: 'United States' },\n { value: 'uk', label: 'United Kingdom' },\n { value: 'ca', label: 'Canada' },\n { value: 'au', label: 'Australia' },\n ],\n props: {\n filter: true,\n showClear: true,\n placeholder: 'Select a country',\n },\n}\n```\n\n**Props (PrimeNG-Specific):**\n\n| Prop | Type | Default | Description |\n| ------------- | --------- | ------- | ------------------------- |\n| `filter` | `boolean` | `false` | Enable search/filter |\n| `showClear` | `boolean` | `false` | Show clear button |\n| `multiple` | `boolean` | `false` | Enable multiple selection |\n| `placeholder` | `string` | - | Dropdown placeholder text |\n| `hint` | `string` | - | Help text below select |\n| `styleClass` | `string` | - | CSS class for dropdown |\n\n#### Radio\n\nRadio button group for selecting a single option.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4202/#/examples/radio\" class=\"example-frame\" title=\"Radio Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'size',\n type: 'radio',\n value: '',\n label: 'Select Size',\n required: true,\n options: [\n { value: 'small', label: 'Small' },\n { value: 'medium', label: 'Medium' },\n { value: 'large', label: 'Large' },\n ],\n props: {\n hint: 'Choose your preferred size',\n },\n}\n```\n\n**Props (PrimeNG-Specific):**\n\n| Prop | Type | Default | Description |\n| ------------ | -------- | ------- | ------------------------------- |\n| `hint` | `string` | - | Help text displayed below group |\n| `styleClass` | `string` | - | CSS class for radio group |\n\n#### Checkbox\n\nBoolean checkbox control for single true/false selections.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4202/#/examples/checkbox\" class=\"example-frame\" title=\"Checkbox Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'terms',\n type: 'checkbox',\n value: false,\n label: 'I accept the terms and conditions',\n required: true,\n props: {\n binary: true,\n },\n}\n```\n\n**Props (PrimeNG-Specific):**\n\n| Prop | Type | Default | Description |\n| ------------ | --------- | ------- | ----------------------------- |\n| `binary` | `boolean` | `true` | Treat as boolean (true/false) |\n| `trueValue` | `any` | `true` | Value when checked |\n| `falseValue` | `any` | `false` | Value when unchecked |\n| `styleClass` | `string` | - | CSS class for checkbox |\n\n#### Multi-Checkbox\n\nMultiple checkbox selection field for choosing multiple options.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4202/#/examples/multi-checkbox\" class=\"example-frame\" title=\"Multi-Checkbox Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'interests',\n type: 'multi-checkbox',\n value: [],\n label: 'Select Your Interests',\n required: true,\n options: [\n { value: 'sports', label: 'Sports' },\n { value: 'music', label: 'Music' },\n { value: 'reading', label: 'Reading' },\n { value: 'travel', label: 'Travel' },\n { value: 'cooking', label: 'Cooking' },\n ],\n props: {\n hint: 'Select all that apply',\n },\n}\n```\n\n**Props (PrimeNG-Specific):**\n\n| Prop | Type | Default | Description |\n| ------------ | -------- | ------- | ------------------------------- |\n| `hint` | `string` | - | Help text displayed below group |\n| `styleClass` | `string` | - | CSS class for checkbox group |\n\n---\n\n### Interactive Fields\n\nInteractive fields provide advanced user input controls.\n\n#### Toggle\n\nSlide toggle switch (InputSwitch) for boolean on/off selections.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4202/#/examples/toggle\" class=\"example-frame\" title=\"Toggle Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'notifications',\n type: 'toggle',\n value: false,\n label: 'Enable email notifications',\n props: {\n hint: 'Receive updates via email',\n },\n}\n```\n\n**Props (PrimeNG-Specific):**\n\n| Prop | Type | Default | Description |\n| ------------ | -------- | ------- | ---------------------- |\n| `hint` | `string` | - | Help text below toggle |\n| `styleClass` | `string` | - | CSS class for toggle |\n\n#### Slider\n\nNumeric slider control for selecting values from a range.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4202/#/examples/slider\" class=\"example-frame\" title=\"Slider Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'volume',\n type: 'slider',\n value: 50,\n label: 'Volume',\n minValue: 0,\n maxValue: 100,\n step: 5,\n props: {\n hint: 'Adjust audio volume',\n },\n}\n```\n\n**Field Properties:**\n\n| Property | Type | Default | Description |\n| ---------- | -------- | ------- | -------------- |\n| `minValue` | `number` | `0` | Minimum value |\n| `maxValue` | `number` | `100` | Maximum value |\n| `step` | `number` | `1` | Increment step |\n\n**Props (PrimeNG-Specific):**\n\n| Prop | Type | Default | Description |\n| ------------- | ---------------------------- | -------------- | --------------------------- |\n| `orientation` | `'horizontal' \\| 'vertical'` | `'horizontal'` | Slider orientation |\n| `range` | `boolean` | `false` | Enable range mode (2 knobs) |\n| `hint` | `string` | - | Help text below slider |\n| `styleClass` | `string` | - | CSS class for slider |\n\n#### Datepicker\n\nDate selection field with calendar popup (p-calendar).\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4202/#/examples/datepicker\" class=\"example-frame\" title=\"Datepicker Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n key: 'birthdate',\n type: 'datepicker',\n value: null,\n label: 'Date of Birth',\n required: true,\n minDate: new Date('1900-01-01'),\n maxDate: new Date(),\n props: {\n dateFormat: 'mm/dd/yy',\n showIcon: true,\n showButtonBar: true,\n hint: 'Select your birth date',\n },\n}\n```\n\n**Field Properties:**\n\n| Property | Type | Description |\n| --------- | ------------------------ | ----------------------- |\n| `minDate` | `Date \\| string \\| null` | Minimum selectable date |\n| `maxDate` | `Date \\| string \\| null` | Maximum selectable date |\n\n**Props (PrimeNG-Specific):**\n\n| Prop | Type | Default | Description |\n| --------------- | ----------------------------------- | ------------ | ------------------------ |\n| `dateFormat` | `string` | `'mm/dd/yy'` | Date format string |\n| `showIcon` | `boolean` | `false` | Show calendar icon |\n| `showButtonBar` | `boolean` | `false` | Show today/clear buttons |\n| `inline` | `boolean` | `false` | Display calendar inline |\n| `selectionMode` | `'single' \\| 'multiple' \\| 'range'` | `'single'` | Date selection mode |\n| `view` | `'date' \\| 'month' \\| 'year'` | `'date'` | Initial calendar view |\n| `touchUI` | `boolean` | `false` | Touch-optimized UI |\n| `hint` | `string` | - | Help text below field |\n| `styleClass` | `string` | - | CSS class for datepicker |\n\n---\n\n### Buttons & Actions\n\nAction buttons provide form submission and navigation controls.\n\n#### Submit Button\n\nForm submission button that's automatically disabled when the form is invalid.\n\n**Live Demo:**\n\n<iframe src=\"http://localhost:4202/#/examples/button\" class=\"example-frame\" title=\"Button Demo\"></iframe>\n\n**Basic Usage:**\n\n```typescript\n{\n type: 'submit',\n key: 'submit',\n label: 'Create Account',\n props: {\n severity: 'primary',\n icon: 'pi pi-check',\n iconPos: 'right',\n },\n}\n```\n\nThe submit button automatically:\n\n- Disables when the form is invalid\n- Emits a `SubmitEvent` when clicked\n- Validates all fields before submission\n\n**Props:**\n\n| Prop | Type | Default | Description |\n| ---------- | ----------------------------------------------------------------------------------------------- | ----------- | --------------- |\n| `severity` | `'primary' \\| 'secondary' \\| 'success' \\| 'info' \\| 'warn' \\| 'help' \\| 'danger' \\| 'contrast'` | `'primary'` | Button theme |\n| `outlined` | `boolean` | `false` | Outlined style |\n| `text` | `boolean` | `false` | Text-only style |\n| `raised` | `boolean` | `false` | Raised style |\n| `rounded` | `boolean` | `false` | Rounded style |\n| `icon` | `string` | - | Icon class |\n| `iconPos` | `'left' \\| 'right' \\| 'top' \\| 'bottom'` | `'left'` | Icon position |\n\n#### Navigation Buttons\n\nNavigation buttons for multi-step (paged) forms.\n\n**Basic Usage:**\n\n```typescript\n{\n fields: [\n {\n key: 'step1',\n type: 'page',\n fields: [\n { key: 'step1Title', type: 'text', label: 'Personal Information', props: { elementType: 'h3' } },\n { key: 'step1Desc', type: 'text', label: 'Tell us about yourself' },\n { key: 'firstName', type: 'input', value: '', label: 'First Name', required: true },\n { key: 'lastName', type: 'input', value: '', label: 'Last Name', required: true },\n {\n type: 'next',\n key: 'next',\n label: 'Continue',\n props: { severity: 'primary', icon: 'pi pi-arrow-right', iconPos: 'right' },\n },\n ],\n },\n {\n key: 'step2',\n type: 'page',\n fields: [\n { key: 'step2Title', type: 'text', label: 'Contact Information', props: { elementType: 'h3' } },\n { key: 'email', type: 'input', value: '', label: 'Email', required: true, email: true },\n { key: 'phone', type: 'input', value: '', label: 'Phone', props: { type: 'tel' } },\n {\n key: 'navigation',\n type: 'row',\n fields: [\n { type: 'previous', key: 'back', label: 'Back', props: { icon: 'pi pi-arrow-left' } },\n { type: 'submit', key: 'submit', label: 'Complete', props: { severity: 'primary' } },\n ],\n },\n ],\n },\n ],\n}\n```\n\n**Button Types:**\n\n- **Next Button**: Navigates to the next page. Automatically disabled when current page has validation errors.\n- **Previous Button**: Navigates to the previous page. Always enabled to allow users to go back.\n\n#### Custom Action Button\n\nGeneric button for custom events. Use this for application-specific actions.\n\n**Basic Usage:**\n\n```typescript\nimport { FormEvent } from '@ng-forge/dynamic-forms';\n\n// Define your custom event\nclass SaveDraftEvent implements FormEvent {\n readonly type = 'save-draft' as const;\n}\n\nconst config = {\n fields: [\n { key: 'title', type: 'input', value: '', label: 'Document Title', required: true },\n { key: 'content', type: 'textarea', value: '', label: 'Content' },\n {\n key: 'actions',\n type: 'row',\n fields: [\n {\n type: 'button',\n key: 'saveDraft',\n label: 'Save as Draft',\n event: SaveDraftEvent,\n props: {\n severity: 'secondary',\n icon: 'pi pi-save',\n },\n },\n {\n type: 'submit',\n key: 'publish',\n label: 'Publish',\n props: {\n severity: 'primary',\n icon: 'pi pi-upload',\n },\n },\n ],\n },\n ],\n} as const satisfies FormConfig;\n```\n\nThen listen for the event in your component:\n\n```typescript\nimport { EventBus } from '@ng-forge/dynamic-forms';\n\nclass MyComponent {\n private eventBus = inject(EventBus);\n\n ngOnInit() {\n this.eventBus.on<SaveDraftEvent>('SaveDraft').subscribe(() => {\n console.log('Save draft clicked', this.form.value);\n // Handle draft saving logic\n });\n }\n}\n```\n\n---\n\n## Theming\n\nPrimeNG supports extensive theming through CSS variables and theme presets. Customize your theme using the provider:\n\n```typescript\nimport { providePrimeNG } from 'primeng/config';\nimport Aura from '@primeuix/themes/aura';\nimport Lara from '@primeuix/themes/lara';\nimport Nora from '@primeuix/themes/nora';\n\nprovidePrimeNG({\n theme: {\n preset: Aura, // or Lara, Nora, etc.\n options: {\n darkModeSelector: '.dark-mode',\n },\n },\n});\n```\n\n### Severity Colors\n\nControl button and component colors using the `severity` prop:\n\n```typescript\n{\n type: 'submit',\n key: 'submit',\n label: 'Submit',\n props: {\n severity: 'primary', // 'primary' | 'secondary' | 'success' | 'info' | 'warn' | 'help' | 'danger' | 'contrast'\n },\n}\n```\n\n### Size Variants\n\nAdjust component sizes using the `size` prop:\n\n```typescript\n{\n key: 'email',\n type: 'input',\n label: 'Email',\n props: {\n size: 'small', // 'small' | 'large' | undefined (default/medium)\n },\n}\n```\n\n### Custom Styling\n\nAdd custom CSS classes to any component:\n\n```typescript\n{\n key: 'email',\n type: 'input',\n label: 'Email',\n props: {\n styleClass: 'my-custom-input',\n },\n}\n```\n\nFor more information, see the [PrimeNG Theming Guide](https://primeng.org/theming).\n\n## Accessibility\n\nAll PrimeNG components include:\n\n- Proper ARIA attributes\n- Keyboard navigation support\n- Screen reader compatibility\n- Focus management\n- Error announcements\n\nPrimeNG components are designed with accessibility in mind and follow WAI-ARIA standards.\n\n## Next Steps\n\n- Check out [Examples & Patterns](../../../examples/) for real-world use cases\n- Learn about [Validation](../../validation/basics/) for form validation\n- See [Type Safety](../../advanced/type-safety/basics/) for TypeScript integration\n- Explore [Conditional Logic](../../dynamic-behavior/conditional-logic/overview/) for dynamic field behavior"
249
- },
250
- {
251
- id: "validation-advanced",
252
- title: "Validation Advanced",
253
- category: "validation",
254
- content: "Advanced validation techniques including conditional validators, dynamic values, and cross-field validation.\n\n## Validators Array\n\nThe `validators` array provides fine-grained control over validation behavior:\n\n```typescript\n{\n key: 'discount',\n type: 'input',\n value: 0,\n validators: [{\n type: 'required',\n }, {\n type: 'min',\n value: 0,\n }, {\n type: 'max',\n value: 100,\n when: {\n type: 'fieldValue',\n fieldPath: 'discountType',\n operator: 'equals',\n value: 'percentage',\n },\n }],\n}\n```\n\n## Conditional Validators\n\nActivate validators only when conditions are met.\n\n### Based on Field Value\n\n```typescript\n{\n key: 'discount',\n type: 'input',\n value: 0,\n validators: [{\n type: 'max',\n value: 100,\n when: {\n type: 'fieldValue',\n fieldPath: 'discountType',\n operator: 'equals',\n value: 'percentage',\n },\n }],\n}\n```\n\nThe max validator only applies when `discountType === 'percentage'`.\n\n### Based on Form Value\n\nValidate against the entire form state:\n\n```typescript\n{\n key: 'endDate',\n type: 'datepicker',\n value: null,\n validators: [{\n type: 'required',\n when: {\n type: 'formValue',\n expression: 'formValue.hasEndDate === true',\n },\n }],\n}\n```\n\n## Dynamic Validator Values\n\nUse JavaScript expressions for dynamic validation:\n\n```typescript\n{\n key: 'quantity',\n type: 'input',\n value: 0,\n validators: [{\n type: 'max',\n expression: 'formValue.maxQuantity || 100',\n }],\n}\n```\n\nThe max value comes from `formValue.maxQuantity`, defaulting to 100.\n\n## Multiple Conditional Validators\n\nCombine multiple validators with different conditions:\n\n```typescript\n{\n key: 'customerId',\n type: 'input',\n value: '',\n validators: [\n {\n type: 'required',\n when: {\n type: 'fieldValue',\n fieldPath: 'customerType',\n operator: 'equals',\n value: 'existing',\n },\n },\n {\n type: 'pattern',\n value: '^[A-Z0-9]{8}$',\n when: {\n type: 'fieldValue',\n fieldPath: 'customerType',\n operator: 'equals',\n value: 'existing',\n },\n },\n ],\n}\n```\n\n## Cross-Field Validation\n\nFor validators that need to compare multiple fields (like password confirmation or date ranges), use custom validators.\n\n**Quick example using expressions:**\n\n```typescript\n{\n key: 'confirmPassword',\n type: 'input',\n validators: [{\n type: 'custom',\n expression: 'fieldValue === formValue.password',\n kind: 'passwordMismatch',\n }],\n validationMessages: {\n passwordMismatch: 'Passwords must match',\n },\n}\n```\n\nSee **[Custom Validators](../custom-validators)** for complete documentation on:\n\n- Expression-based validators (simple, inline expressions)\n- Function-based validators (complex, reusable logic)\n- Cross-field validation patterns\n- FieldContext API for accessing other field values\n- Async validators and HTTP validators\n\n## Complex Conditional Logic\n\nCombine multiple conditions with AND/OR logic:\n\n```typescript\n{\n key: 'businessEmail',\n type: 'input',\n value: '',\n validators: [{\n type: 'required',\n when: {\n type: 'and',\n conditions: [\n {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n {\n type: 'fieldValue',\n fieldPath: 'hasTeam',\n operator: 'equals',\n value: true,\n },\n ],\n },\n }],\n}\n```\n\n## Complete Examples\n\n### Conditional Business Form\n\n```typescript\nconst config = {\n fields: [\n {\n key: 'accountType',\n type: 'radio',\n value: 'personal',\n options: [\n { value: 'personal', label: 'Personal' },\n { value: 'business', label: 'Business' },\n ],\n },\n {\n key: 'companyName',\n type: 'input',\n value: '',\n validators: [\n {\n type: 'required',\n when: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n },\n {\n type: 'minLength',\n value: 2,\n when: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n },\n ],\n },\n {\n key: 'taxId',\n type: 'input',\n value: '',\n validators: [\n {\n type: 'required',\n when: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n },\n {\n type: 'pattern',\n value: '^[0-9]{2}-[0-9]{7}$',\n when: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n },\n ],\n },\n ],\n} as const satisfies FormConfig;\n```\n\n### Dynamic Validation Form\n\nForm where validation rules change based on selections:\n\n```typescript\nconst config = {\n fields: [\n {\n key: 'discountType',\n type: 'select',\n value: '',\n options: [\n { value: 'percentage', label: 'Percentage' },\n { value: 'fixed', label: 'Fixed Amount' },\n ],\n },\n {\n key: 'discountValue',\n type: 'input',\n value: 0,\n required: true,\n validators: [\n {\n type: 'min',\n value: 0,\n },\n {\n type: 'max',\n value: 100,\n when: {\n type: 'fieldValue',\n fieldPath: 'discountType',\n operator: 'equals',\n value: 'percentage',\n },\n },\n {\n type: 'max',\n expression: 'formValue.orderTotal || 1000',\n when: {\n type: 'fieldValue',\n fieldPath: 'discountType',\n operator: 'equals',\n value: 'fixed',\n },\n },\n ],\n props: { type: 'number' },\n },\n ],\n} as const satisfies FormConfig;\n```\n\n## Best Practices\n\n**Use shorthand when possible:**\n\n```typescript\n// \u2705 Good - Simple and clear\n{ required: true, email: true }\n\n// \u274C Avoid - Unnecessarily complex\n{ validators: [{ type: 'required' }, { type: 'email' }] }\n```\n\n**Combine shorthand with validators array:**\n\n```typescript\n// \u2705 Good - Best of both\n{\n required: true,\n email: true,\n validators: [{\n type: 'pattern',\n value: '@company\\\\.com$',\n when: { /* condition */ },\n }],\n}\n```\n\n**Keep conditions simple:**\n\n```typescript\n// \u2705 Good - Easy to understand\nwhen: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n}\n\n// \u274C Avoid - Overly complex\nwhen: {\n type: 'and',\n conditions: [/* 5 nested conditions */],\n}\n```\n\n## Related\n\n- **[Validation Basics](../basics/)** - Shorthand validators\n- **[Validation Reference](../reference/)** - Complete API\n- **[Conditional Logic](../../dynamic-behavior/conditional-logic/overview/)** - Field behavior changes\n- **[Examples](../../examples/)** - Real-world patterns"
255
- },
256
- {
257
- id: "validation-basics",
258
- title: "Validation Basics",
259
- category: "validation",
260
- content: "Dynamic Forms provides powerful, type-safe validation that integrates directly with Angular's signal forms. Start with simple shorthand validators and progress to advanced conditional validation as your needs grow.\n\n## Signal Forms Integration\n\nDynamic Forms validation maps directly to Angular's signal forms validators:\n\n```typescript\n// Your configuration\n{ key: 'email', type: 'input', value: '', required: true, email: true }\n\n// Becomes\nimport { required, email } from '@angular/forms/signals';\nrequired(fieldPath);\nemail(fieldPath);\n```\n\nThis tight integration means:\n\n- **Zero overhead** - Direct signal forms API usage\n- **Familiar patterns** - Same validators you know\n- **Full type safety** - TypeScript inference throughout\n\n## Which Validation Approach Should I Use?\n\nChoose based on your validation complexity:\n\n### Shorthand Validators\n\n**Use when:** Simple, always-active validation\n\n```typescript\n{\n key: 'email',\n type: 'input',\n value: '',\n required: true,\n email: true,\n minLength: 5,\n}\n```\n\n**Benefits:**\n\n- Concise and readable\n- Perfect for common validations\n- Type-safe with full IntelliSense\n\n### Validators Array\n\n**Use when:** Conditional validation or custom messages\n\n```typescript\n{\n key: 'discount',\n type: 'input',\n value: 0,\n validators: [{\n type: 'max',\n value: 100,\n when: {\n type: 'fieldValue',\n fieldPath: 'discountType',\n operator: 'equals',\n value: 'percentage',\n },\n }],\n}\n```\n\n**Benefits:**\n\n- Conditional validation\n- Custom error messages\n- Dynamic validator values\n\n### Logic Array\n\n**Use when:** Changing field behavior (hidden/required/disabled)\n\n```typescript\n{\n key: 'taxId',\n type: 'input',\n value: '',\n logic: [{\n type: 'required',\n condition: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n }],\n}\n```\n\n**Benefits:**\n\n- Dynamic field behavior\n- Conditional required fields\n- See [Conditional Logic](../../dynamic-behavior/conditional-logic/overview/) for details\n\n## Shorthand Validators\n\n### required\n\nMark a field as mandatory:\n\n```typescript\n{\n key: 'name',\n type: 'input',\n value: '',\n required: true,\n}\n```\n\n### email\n\nValidate email format:\n\n```typescript\n{\n key: 'email',\n type: 'input',\n value: '',\n required: true,\n email: true,\n}\n```\n\n### minLength / maxLength\n\nValidate string length:\n\n```typescript\n{\n key: 'username',\n type: 'input',\n value: '',\n minLength: 3,\n maxLength: 20,\n}\n```\n\n### min / max\n\nValidate numeric range:\n\n```typescript\n{\n key: 'age',\n type: 'input',\n value: null,\n min: 18,\n max: 120,\n props: { type: 'number' },\n}\n```\n\n### pattern\n\nValidate with regular expressions:\n\n```typescript\n{\n key: 'zipCode',\n type: 'input',\n value: '',\n pattern: '^[0-9]{5}$', // 5-digit US ZIP code\n}\n```\n\n## Combining Validators\n\nStack multiple validators on the same field:\n\n```typescript\n{\n key: 'password',\n type: 'input',\n value: '',\n required: true,\n minLength: 8,\n maxLength: 128,\n pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\\\d)',\n props: { type: 'password' },\n}\n```\n\nAll validators must pass for the field to be valid.\n\n## Validation Messages\n\n### Default Messages\n\nEach validator has a built-in error message:\n\n```typescript\n{\n required: true;\n} // \u2192 \"This field is required\"\n{\n email: true;\n} // \u2192 \"Please enter a valid email address\"\n{\n minLength: 8;\n} // \u2192 \"Minimum length is 8 characters\"\n```\n\n### Custom Messages\n\nOverride default messages for better UX:\n\n```typescript\n{\n key: 'email',\n type: 'input',\n value: '',\n required: true,\n email: true,\n validationMessages: {\n required: 'Email address is required',\n email: 'Please enter a valid email address',\n },\n}\n```\n\n### Dynamic Messages\n\nUse signals or observables for i18n:\n\n```typescript\n{\n key: 'email',\n type: 'input',\n value: '',\n required: true,\n validationMessages: {\n required: this.transloco.selectTranslate('validation.required'),\n email: this.transloco.selectTranslate('validation.email'),\n },\n}\n```\n\n## Quick Examples\n\n### User Registration\n\n```typescript\nconst config = {\n fields: [\n {\n key: 'username',\n type: 'input',\n value: '',\n required: true,\n minLength: 3,\n maxLength: 20,\n pattern: '^[a-zA-Z0-9_]+$',\n validationMessages: {\n required: 'Username is required',\n minLength: 'Username must be at least 3 characters',\n maxLength: 'Username cannot exceed 20 characters',\n pattern: 'Username can only contain letters, numbers, and underscores',\n },\n },\n {\n key: 'email',\n type: 'input',\n value: '',\n required: true,\n email: true,\n validationMessages: {\n required: 'Email is required',\n email: 'Please enter a valid email address',\n },\n },\n {\n key: 'password',\n type: 'input',\n value: '',\n required: true,\n minLength: 8,\n pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\\\d)',\n validationMessages: {\n required: 'Password is required',\n minLength: 'Password must be at least 8 characters',\n pattern: 'Password must include uppercase, lowercase, and a number',\n },\n props: { type: 'password' },\n },\n ],\n} as const satisfies FormConfig;\n```\n\n## When Validation Runs\n\nValidation occurs:\n\n- **On blur** - When user leaves a field\n- **On change** - As user types (after first blur)\n- **On submit** - When form is submitted\n\nInvalid fields prevent form submission and display error messages.\n\n## Next Steps\n\n- **[Validation Advanced](../advanced/)** - Conditional validation, dynamic values\n- **[Validation Reference](../reference/)** - Complete validator API\n- **[Conditional Logic](../../dynamic-behavior/conditional-logic/overview/)** - Dynamic field behavior\n- **[Examples](../../examples/)** - Real-world validation patterns"
261
- },
262
- {
263
- id: "validation-custom-validators",
264
- title: "Validation Custom Validators",
265
- category: "validation",
266
- content: "Custom validation functions for complex validation logic that goes beyond built-in validators.\n\n## Overview\n\nng-forge supports four types of custom validators:\n\n1. **Declarative HTTP** (`type: 'http'`) - Validate against an HTTP endpoint with no function registration\n2. **CustomValidator** - Synchronous validators with access to FieldContext\n3. **AsyncCustomValidator** - Async validators using Angular's resource API\n4. **HttpCustomValidator** (function-based) - HTTP validators with full programmatic control\n\n**Key Principle:** Validators should focus on validation logic, NOT presentation. Return only the error `kind` and configure messages at field level for proper i18n support.\n\n## Live Demo\n\nTry the interactive example below to see both expression-based and function-based validators in action:\n\n<iframe src=\"http://localhost:4201/#/examples/expression-validators-demo\" class=\"example-frame\" title=\"Expression Validators Demo\"></iframe>\n\n## Message Resolution (STRICT)\n\nAll error messages MUST be explicitly configured. The framework enforces this strictly:\n\n1. **Field-level `validationMessages[kind]`** (highest priority - per-field customization)\n2. **Form-level `defaultValidationMessages[kind]`** (fallback for common messages)\n3. **No message configured = Console warning + error NOT displayed**\n\n**Important:** Validator-returned messages are NOT used as fallbacks. This enforces proper separation of concerns and i18n patterns.\n\nYou can define messages at the form level for common validation errors:\n\n```typescript\n{\n defaultValidationMessages: {\n noSpaces: 'Spaces are not allowed',\n passwordMismatch: 'Passwords must match'\n },\n fields: [/* ... */]\n}\n```\n\n## Two Validator Patterns\n\nng-forge supports two patterns for custom validators:\n\n1. **Function-based** - Register reusable validator functions (best for complex logic, reusability)\n2. **Expression-based** - Inline JavaScript expressions (best for simple, one-off validations)\n\n## Expression-Based Validators\n\nFor simple validation logic, use inline JavaScript expressions without registering functions.\n\n### Basic Example\n\n```typescript\n{\n key: 'confirmPassword',\n type: 'input',\n value: '',\n validators: [{\n type: 'custom',\n expression: 'fieldValue === formValue.password',\n kind: 'passwordMismatch',\n }],\n validationMessages: {\n passwordMismatch: 'Passwords must match',\n },\n}\n```\n\n**How it works:**\n\n- `fieldValue` - Current field's value\n- `formValue` - Entire form value object\n- Expression returns `true` = validation passes\n- Expression returns `false` = validation fails with the specified `kind`\n\n### Available Context\n\nExpression-based validators have access to:\n\n- **`fieldValue`** - Current field value\n- **`formValue`** - Complete form value object (e.g., `formValue.password`, `formValue.email`)\n- **`fieldPath`** - Current field path\n- Custom functions registered in `customFnConfig.customFunctions`\n\n### Safe Member Access\n\n**Built-in null/undefined handling**: Member access is safe by default - no manual null checks needed!\n\n```typescript\n// \u2705 Works safely even when nested values are null/undefined\n{\n expression: 'fieldValue !== formValue.user.profile.firstName',\n kind: 'invalidNested',\n}\n\n// \u274C Unnecessary - Don't do this\n{\n expression: '!formValue.user || !formValue.user.profile || !formValue.user.profile.firstName || fieldValue !== formValue.user.profile.firstName',\n kind: 'invalidNested',\n}\n\n// \u2705 Better - Safe by default\n{\n expression: '!formValue.user.profile.firstName || fieldValue !== formValue.user.profile.firstName',\n kind: 'invalidNested',\n}\n```\n\nAccessing properties on `null` or `undefined` returns `undefined` instead of throwing errors, making expressions cleaner and more maintainable.\n\n### Common Expression Patterns\n\n**Password confirmation:**\n\n```typescript\n{\n expression: 'fieldValue === formValue.password',\n kind: 'passwordMismatch',\n}\n```\n\n**Date range validation:**\n\n```typescript\n{\n expression: 'new Date(fieldValue) > new Date(formValue.startDate)',\n kind: 'endDateBeforeStart',\n}\n```\n\n**Conditional required:**\n\n```typescript\n{\n expression: 'formValue.requiresApproval ? fieldValue?.length > 0 : true',\n kind: 'approvalRequired',\n}\n```\n\n**Numeric comparison:**\n\n```typescript\n{\n expression: 'fieldValue >= formValue.minAge && fieldValue <= formValue.maxAge',\n kind: 'ageOutOfRange',\n}\n```\n\n**Deeply nested field validation:**\n\n```typescript\n{\n // Safe to access deeply nested properties\n expression: 'fieldValue.toLowerCase() !== formValue.user.address.city.toLowerCase()',\n kind: 'invalidAddress',\n}\n```\n\n### Security\n\nExpressions use **secure AST-based parsing**. Only safe JavaScript operations are allowed.\n\n## Function-Based Validators\n\nBest for validation that needs field value and access to other fields via FieldContext.\n\n### Basic Example\n\n```typescript\nimport { CustomValidator } from '@ng-forge/dynamic-forms';\n\n// \u2705 RECOMMENDED: Return only kind\nconst noSpaces: CustomValidator = (ctx) => {\n const value = ctx.value();\n if (typeof value === 'string' && value.includes(' ')) {\n return { kind: 'noSpaces' }; // No hardcoded message\n }\n return null;\n};\n\n// Register and configure message\nconst config = {\n fields: [\n {\n key: 'username',\n type: 'input',\n validators: [{ type: 'custom', functionName: 'noSpaces' }],\n validationMessages: {\n noSpaces: 'Spaces are not allowed', // Or Observable/Signal for i18n\n },\n },\n ],\n customFnConfig: {\n validators: {\n noSpaces,\n },\n },\n};\n```\n\n### FieldContext API\n\nThe `FieldContext` API provides access to:\n\n- **`ctx.value()`** - Current field value (signal)\n- **`ctx.state`** - Field state (errors, touched, dirty, etc.)\n- **`ctx.valueOf(path)`** - Access other field values (PUBLIC API for cross-field validation)\n- **`ctx.stateOf(path)`** - Access other field states\n- **`ctx.field`** - Current field tree\n\n### Cross-Field Validation\n\nUse `ctx.valueOf()` to access other field values for comparison validators:\n\n```typescript\nimport { CustomValidator } from '@ng-forge/dynamic-forms';\n\nconst greaterThanMin: CustomValidator = (ctx) => {\n const value = ctx.value();\n const minValue = ctx.valueOf('minAge');\n\n if (minValue !== undefined && value <= minValue) {\n return { kind: 'notGreaterThanMin' };\n }\n return null;\n};\n\n// Note: Custom validators return only 'kind'. Built-in validators (min, max, etc.)\n// automatically include params for interpolation (e.g., {{min}}, {{max}}, etc.)\nconst config = {\n fields: [\n { key: 'minAge', type: 'input', value: 0 },\n {\n key: 'maxAge',\n type: 'input',\n value: 0,\n validators: [\n {\n type: 'custom',\n functionName: 'greaterThanMin',\n },\n ],\n validationMessages: {\n notGreaterThanMin: 'Maximum age must be greater than minimum age',\n },\n },\n ],\n customFnConfig: {\n validators: { greaterThanMin },\n },\n};\n```\n\n**Common Cross-Field Patterns:**\n\n- Password confirmation matching\n- Date range validation (start < end)\n- Numeric range validation (min < max)\n- Conditional required fields\n\n### Password Confirmation Example\n\n```typescript\nconst passwordMatch: CustomValidator = (ctx) => {\n const confirmPassword = ctx.value();\n const password = ctx.valueOf('password');\n\n if (!confirmPassword || !password) {\n return null; // Let required validator handle empty case\n }\n\n if (password !== confirmPassword) {\n return { kind: 'passwordMismatch' };\n }\n return null;\n};\n\n// In field config:\n{\n key: 'confirmPassword',\n type: 'input',\n validators: [{ type: 'custom', functionName: 'passwordMatch' }],\n validationMessages: {\n passwordMismatch: 'Passwords do not match'\n }\n}\n```\n\n## Async Validators (Resource-based)\n\n> **Deprecation notice:** The old `type: 'customAsync'` still works but is deprecated. Use `type: 'async'` instead.\n\nAsync validators use Angular's resource API for database lookups or complex async operations.\n\n### Basic Example\n\n```typescript\nimport { AsyncCustomValidator } from '@ng-forge/dynamic-forms';\nimport { inject } from '@angular/core';\nimport { rxResource } from '@angular/core/rxjs-interop';\nimport { of } from 'rxjs';\nimport { UserService } from './user.service';\n\nconst checkUsernameAvailable: AsyncCustomValidator = {\n // Extract params from field context\n params: (ctx) => ({ username: ctx.value() }),\n\n // Create resource with params signal\n factory: (params) => {\n const userService = inject(UserService);\n return rxResource({\n request: params,\n loader: ({ request }) => {\n if (!request?.username) return of(null);\n return userService.checkAvailability(request.username);\n },\n });\n },\n\n // Map result to validation error\n onSuccess: (result, ctx) => {\n if (!result) return null;\n return result.available ? null : { kind: 'usernameTaken' };\n },\n\n // Handle errors gracefully\n onError: (error, ctx) => {\n console.error('Availability check failed:', error);\n return null; // Don't block form on network errors\n },\n};\n\nconst config = {\n fields: [\n {\n key: 'username',\n type: 'input',\n validators: [{ type: 'async', functionName: 'checkUsernameAvailable' }],\n validationMessages: {\n usernameTaken: 'This username is already taken',\n },\n },\n ],\n customFnConfig: {\n asyncValidators: {\n checkUsernameAvailable,\n },\n },\n};\n```\n\n**Key Benefits:**\n\n- Automatic loading states via resource API\n- Angular manages resource lifecycle\n- Reactive - refetches when params change\n- Integrates with Signal Forms validation state\n\n### Structure\n\n```typescript\ninterface AsyncCustomValidator<TValue, TParams, TResult> {\n // Function that receives field context and returns resource params\n readonly params: (ctx: FieldContext<TValue>, config?: Record<string, unknown>) => TParams;\n\n // Function that creates a ResourceRef from the params signal\n readonly factory: (params: Signal<TParams | undefined>) => ResourceRef<TResult | undefined>;\n\n // Map successful resource result to validation errors\n readonly onSuccess?: (result: TResult, ctx: FieldContext<TValue>) => ValidationError | ValidationError[] | null;\n\n // Handle resource errors\n readonly onError?: (error: unknown, ctx: FieldContext<TValue>) => ValidationError | ValidationError[] | null;\n}\n```\n\n## Declarative HTTP Validators\n\nThe simplest way to validate against an HTTP endpoint. No function registration required \u2014 configure the request and response mapping inline.\n\n### Basic Example\n\n```typescript\n{\n key: 'username',\n type: 'input',\n validators: [\n {\n type: 'http',\n http: {\n url: '/api/users/check-availability',\n method: 'GET',\n queryParams: {\n username: 'fieldValue', // fieldValue refers to the current field\n },\n },\n responseMapping: {\n validWhen: 'response.available', // Truthy = valid\n errorKind: 'usernameTaken',\n },\n },\n ],\n validationMessages: {\n usernameTaken: 'This username is already taken',\n },\n}\n```\n\n**Key points:**\n\n- `fieldValue` is available as an expression in `params`, `queryParams`, and `body` (refers to the current field's value)\n- Use `params` with `:key` URL placeholders for path parameters (e.g. `url: '/api/users/:id'` + `params: { id: 'fieldValue' }`)\n- `validWhen` is evaluated with `{ response }` in scope \u2014 truthy means validation passed\n- **Fail-closed:** if the HTTP request errors, the validator returns `{ kind: errorKind }` by default (prevents submission on network failure)\n\n### POST with Body Expressions\n\n```typescript\n{\n type: 'http',\n http: {\n url: '/api/validate-email',\n method: 'POST',\n body: {\n email: 'fieldValue',\n },\n evaluateBodyExpressions: true,\n },\n responseMapping: {\n validWhen: 'response.valid',\n errorKind: 'emailInvalid',\n },\n}\n```\n\n### Error Message Interpolation\n\nUse `errorParams` to include response data in validation messages:\n\n```typescript\n{\n type: 'http',\n http: {\n url: '/api/users/check-availability',\n queryParams: { username: 'fieldValue' },\n },\n responseMapping: {\n validWhen: 'response.available',\n errorKind: 'usernameTaken',\n errorParams: {\n suggestion: 'response.suggestion', // Maps to {{suggestion}} in the message\n },\n },\n}\n// Message: 'Username is taken. Try {{suggestion}}'\n```\n\n### HttpValidationResponseMapping Interface\n\n```typescript\ninterface HttpValidationResponseMapping {\n /** Expression evaluated with { response }. Truthy = valid (no error). */\n validWhen: string;\n\n /** Error kind returned when validWhen is falsy \u2014 maps to validationMessages. */\n errorKind: string;\n\n /**\n * Optional parameters for message interpolation.\n * Keys become {{paramName}} placeholders. Values are expressions evaluated with { response }.\n */\n errorParams?: Record<string, string>;\n}\n```\n\n**When to use declarative vs function-based:**\n\n| Scenario | Use |\n| ----------------------------------------------- | ------------------------------------------------ |\n| Standard availability check | Declarative (`type: 'http'` + `responseMapping`) |\n| Need `inject()` for Angular services | Function-based (`HttpCustomValidator`) |\n| Conditional request (skip based on field state) | Function-based (`HttpCustomValidator`) |\n| Custom error handling (fail-open on error) | Function-based (`HttpCustomValidator`) |\n\n## HTTP Validators (Function-Based)\n\n> **Deprecation notice:** The old `type: 'customHttp'` still works but is deprecated. Use `type: 'http'` with `functionName` instead.\n\nFunction-based HTTP validators give you full programmatic control over request construction and response handling.\n\n### Basic Example\n\n```typescript\nimport { HttpCustomValidator } from '@ng-forge/dynamic-forms';\n\nconst checkEmailDomain: HttpCustomValidator = {\n // Build HTTP request from context\n request: (ctx) => {\n const email = ctx.value();\n if (!email?.includes('@')) return undefined; // Skip if invalid\n\n const domain = email.split('@')[1];\n return {\n url: `/api/validate-domain`,\n method: 'POST',\n body: { domain },\n headers: { 'Content-Type': 'application/json' },\n };\n },\n\n // NOTE: Inverted logic - onSuccess checks if response indicates INVALID\n // We're validating, not fetching data!\n onSuccess: (response, ctx) => {\n // Assuming API returns { valid: boolean }\n return response.valid ? null : { kind: 'invalidDomain' };\n },\n\n onError: (error, ctx) => {\n console.error('Domain validation failed:', error);\n return null; // Don't block form on network errors\n },\n};\n\nconst config = {\n fields: [\n {\n key: 'email',\n type: 'input',\n validators: [{ type: 'http', functionName: 'checkEmailDomain' }],\n validationMessages: {\n invalidDomain: 'This email domain is not allowed',\n },\n },\n ],\n customFnConfig: {\n httpValidators: {\n checkEmailDomain,\n },\n },\n};\n```\n\n**Key Benefits:**\n\n- Automatic request cancellation when field changes\n- Built-in debouncing via resource API\n- Prevents race conditions\n- Optimized for HTTP-specific validation\n\n**Important:** HTTP validators use \"inverted logic\" - `onSuccess` should return an error if validation fails, not if the HTTP request succeeds. You're checking validation status, not fetching data.\n\n### Structure\n\n```typescript\ninterface HttpCustomValidator<TValue, TResult> {\n // Build HTTP request from field context\n readonly request: (ctx: FieldContext<TValue>) => HttpResourceRequest | string | undefined;\n\n // Map successful response to validation error\n readonly onSuccess?: (result: TResult, ctx: FieldContext<TValue>) => ValidationError | ValidationError[] | null;\n\n // Handle HTTP errors\n readonly onError?: (error: unknown, ctx: FieldContext<TValue>) => ValidationError | ValidationError[] | null;\n}\n\ninterface HttpResourceRequest {\n url: string;\n method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n body?: any;\n headers?: Record<string, string | string[]>;\n}\n```\n\n## Conditional Custom Validators\n\nApply validators conditionally using the `when` property with a `ConditionalExpression`:\n\n```typescript\nconst businessEmailValidator: CustomValidator = (ctx) => {\n const value = ctx.value();\n const domain = value?.split('@')[1];\n\n const freeEmailDomains = ['gmail.com', 'yahoo.com', 'hotmail.com'];\n\n if (domain && freeEmailDomains.includes(domain.toLowerCase())) {\n return { kind: 'requireBusinessEmail' };\n }\n return null;\n};\n\nconst config = {\n fields: [\n {\n key: 'accountType',\n type: 'select',\n value: 'personal',\n options: [\n { value: 'personal', label: 'Personal' },\n { value: 'business', label: 'Business' },\n ],\n },\n {\n key: 'email',\n type: 'input',\n validators: [\n {\n type: 'custom',\n functionName: 'businessEmailValidator',\n // Only apply when account type is \"business\"\n when: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n },\n ],\n validationMessages: {\n requireBusinessEmail: 'Please use a business email address',\n },\n },\n ],\n customFnConfig: {\n validators: { businessEmailValidator },\n },\n};\n```\n\nThe validator is only active when the `when` condition evaluates to `true`, allowing dynamic validation based on form state. See [Conditional Logic](../../dynamic-behavior/conditional-logic/overview/) for all expression types and operators.\n\n## Common Validation Patterns\n\n### Email Domain Validation\n\n```typescript\nconst emailDomainValidator: CustomValidator = (ctx) => {\n const blockedDomains = ['tempmail.com', 'throwaway.email'];\n const email = ctx.value();\n const domain = email?.split('@')[1];\n\n if (domain && blockedDomains.includes(domain)) {\n return { kind: 'blockedDomain' };\n }\n return null;\n};\n```\n\n### Age Validation\n\n```typescript\nconst ageValidator: CustomValidator = (ctx) => {\n const birthDate = ctx.value();\n const age = calculateAge(birthDate);\n\n if (age < 18) {\n return { kind: 'tooYoung' };\n }\n return null;\n};\n```\n\n### Conditional Required\n\n```typescript\nconst conditionalRequiredValidator: CustomValidator = (ctx) => {\n const value = ctx.value();\n const employmentStatus = ctx.valueOf('employmentStatus');\n\n // Company name required if employed\n if (employmentStatus === 'employed' && !value) {\n return { kind: 'required' };\n }\n return null;\n};\n```\n\n### Date Range Validation\n\n```typescript\nconst dateRangeValidator: CustomValidator = (ctx) => {\n const endDate = ctx.value();\n const startDate = ctx.valueOf('startDate');\n\n if (startDate && endDate && startDate > endDate) {\n return { kind: 'invalidDateRange' };\n }\n return null;\n};\n```\n\n## Multiple Errors\n\nValidators can return multiple errors for cross-field validation:\n\n```typescript\nconst validateDateRange: CustomValidator = (ctx) => {\n const errors: ValidationError[] = [];\n\n const startDate = ctx.valueOf('startDate');\n const endDate = ctx.valueOf('endDate');\n\n if (!startDate) errors.push({ kind: 'startDateRequired' });\n if (!endDate) errors.push({ kind: 'endDateRequired' });\n if (startDate && endDate && startDate > endDate) {\n errors.push({ kind: 'invalidDateRange' });\n }\n\n return errors.length > 0 ? errors : null;\n};\n```\n\n## Validation Messages\n\n### Field-Level Messages\n\n```typescript\n{\n key: 'username',\n validators: [{ type: 'custom', functionName: 'noSpaces' }],\n validationMessages: {\n noSpaces: 'Spaces are not allowed'\n }\n}\n```\n\n### Form-Level Default Messages\n\n```typescript\n{\n defaultValidationMessages: {\n noSpaces: 'Spaces are not allowed',\n passwordMismatch: 'Passwords must match',\n usernameTaken: 'This username is already taken'\n },\n fields: [/* ... */]\n}\n```\n\n### Dynamic Messages with i18n\n\n```typescript\nimport { inject } from '@angular/core';\nimport { TranslateService } from '@ngx-translate/core';\n\n{\n key: 'username',\n validators: [{ type: 'custom', functionName: 'noSpaces' }],\n validationMessages: {\n noSpaces: inject(TranslateService).get('VALIDATION.NO_SPACES')\n }\n}\n```\n\n### Parameterized Messages\n\nMessages can interpolate params from ValidatorConfig using Angular template syntax (double curly braces around the param name).\n\n**Syntax:** To interpolate a param, wrap its name in double curly braces (same syntax as Angular templates).\n\n**Example:** To access `params.label`, write the param name `label` wrapped in double curly braces in your message string.\n\n```typescript\n{\n validators: [\n {\n type: 'custom',\n functionName: 'lessThanField',\n params: { field: 'minAge', label: 'Minimum Age' }\n }\n ],\n validationMessages: {\n // Interpolate params.label using double curly braces\n notLessThan: 'Must be less than {{label}}'\n }\n}\n```\n\nThe validation message will render as **\"Must be less than Minimum Age\"** by interpolating the `label` param value.\n\n## Type Safety\n\nAll validator types are fully typed. While validators can optionally use generic type parameters for stricter typing, the simple form without generics works well for most cases:\n\n```typescript\n// Simple form - works for most cases\nconst noSpaces: CustomValidator = (ctx) => {\n const value = ctx.value();\n if (typeof value === 'string' && value.includes(' ')) {\n return { kind: 'noSpaces' };\n }\n return null;\n};\n\n// With type parameter - for stricter typing (advanced)\nconst strictNoSpaces: CustomValidator<string> = (ctx) => {\n const value = ctx.value(); // Type: string\n // TypeScript knows value is always string\n return value.includes(' ') ? { kind: 'noSpaces' } : null;\n};\n\n// Async validators with type parameters (advanced)\nconst checkUsername: AsyncCustomValidator<string, { username: string }, { available: boolean }> = {\n params: (ctx) => ({ username: ctx.value() }),\n factory: (params) => {\n /* ... */\n },\n onSuccess: (result, ctx) => {\n result.available; // Type: boolean\n return result.available ? null : { kind: 'usernameTaken' };\n },\n};\n\n// HTTP validators with type parameters (advanced)\nconst checkDomain: HttpCustomValidator<string, { valid: boolean }> = {\n request: (ctx) => ({\n /* ... */\n }),\n onSuccess: (response, ctx) => {\n response.valid; // Type: boolean\n return response.valid ? null : { kind: 'invalidDomain' };\n },\n};\n```\n\n**Note:** When registering validators in `customFnConfig.validators`, use the simple form without type parameters to avoid TypeScript compatibility issues.\n\n## Best Practices\n\n1. **Separation of Concerns**: Return only error `kind`, configure messages separately\n2. **i18n Support**: Use Observable/Signal for validation messages\n3. **Graceful Degradation**: Handle async/HTTP errors without blocking the form\n4. **Cross-Field Validation**: Use `ctx.valueOf()` for accessing related fields\n5. **Type Safety**: Leverage TypeScript generics for type-safe validation\n6. **Message Priority**: Use field-level messages for customization, form-level for common errors\n7. **Conditional Validation**: Use `when` property with `ConditionalExpression` for dynamic validators\n8. **Inverted Logic**: HTTP validators check validity, not data fetching success\n\n## Related Documentation\n\n- [Validation Basics](../basics/) - Core validation concepts\n- [Validation Reference](../reference/) - Standard validation rules\n- [Type Safety](../../advanced/type-safety/basics/) - TypeScript integration"
267
- },
268
- {
269
- id: "validation-reference",
270
- title: "Validation Reference",
271
- category: "validation",
272
- content: "Complete reference for all validators, conditional expressions, and validation configuration options.\n\n## All Validator Types\n\n### required\n\nField must have a value.\n\n```typescript\n// Shorthand\n{ required: true }\n\n// Explicit\n{\n validators: [{\n type: 'required',\n }],\n}\n```\n\n### email\n\nMust be a valid email format.\n\n```typescript\n// Shorthand\n{ email: true }\n\n// Explicit\n{\n validators: [{\n type: 'email',\n }],\n}\n```\n\n### minLength\n\nMinimum string length.\n\n```typescript\n// Shorthand\n{ minLength: 8 }\n\n// Explicit\n{\n validators: [{\n type: 'minLength',\n value: 8,\n }],\n}\n```\n\n### maxLength\n\nMaximum string length.\n\n```typescript\n// Shorthand\n{ maxLength: 20 }\n\n// Explicit\n{\n validators: [{\n type: 'maxLength',\n value: 20,\n }],\n}\n```\n\n### min\n\nMinimum numeric value.\n\n```typescript\n// Shorthand\n{ min: 0 }\n\n// Explicit\n{\n validators: [{\n type: 'min',\n value: 0,\n }],\n}\n```\n\n### max\n\nMaximum numeric value.\n\n```typescript\n// Shorthand\n{ max: 100 }\n\n// Explicit\n{\n validators: [{\n type: 'max',\n value: 100,\n }],\n}\n```\n\n### pattern\n\nRegular expression validation.\n\n```typescript\n// Shorthand\n{ pattern: '^[0-9]{5}$' }\n\n// Explicit\n{\n validators: [{\n type: 'pattern',\n value: '^[0-9]{5}$',\n }],\n}\n```\n\n## Conditional Expressions\n\nValidators support a `when` property for conditional validation. See **[Conditional Logic](../../dynamic-behavior/conditional-logic/overview/)** for the complete reference on:\n\n- All operators (`equals`, `notEquals`, `greater`, `less`, `contains`, `matches`, etc.)\n- Expression types (`fieldValue`, `formValue` (deprecated), `javascript`, `custom`)\n- Combining conditions with `and`/`or` logic\n\n**Quick example:**\n\n```typescript\n{\n validators: [{\n type: 'required',\n when: {\n type: 'fieldValue',\n fieldPath: 'accountType',\n operator: 'equals',\n value: 'business',\n },\n }],\n}\n```\n\n## ValidatorConfig Types\n\nValidatorConfig is a discriminated union type with five variants:\n\n```typescript\n// Built-in validators (required, email, min, max, etc.)\ninterface BuiltInValidatorConfig {\n type: 'required' | 'email' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern';\n value?: number | string | RegExp;\n expression?: string;\n when?: ConditionalExpression;\n}\n\n// Custom synchronous validators\ninterface CustomValidatorConfig {\n type: 'custom';\n functionName?: string;\n params?: Record<string, unknown>;\n expression?: string;\n kind?: string;\n errorParams?: Record<string, string>;\n when?: ConditionalExpression;\n}\n\n// Async validators (for debounced validation, database lookups)\ninterface AsyncValidatorConfig {\n // 'async' is preferred; 'customAsync' is deprecated and emits a warning in dev mode\n type: 'async' | 'customAsync';\n functionName: string;\n params?: Record<string, unknown>;\n when?: ConditionalExpression;\n}\n\n// Function-based HTTP validators (requires a registered function)\ninterface FunctionHttpValidatorConfig {\n // 'http' is preferred; 'customHttp' is deprecated and emits a warning in dev mode\n type: 'http' | 'customHttp';\n functionName: string;\n params?: Record<string, unknown>;\n when?: ConditionalExpression;\n}\n\n// Declarative HTTP validators (fully JSON-serializable, no function registration)\ninterface DeclarativeHttpValidatorConfig {\n type: 'http';\n http: HttpRequestConfig;\n responseMapping: HttpValidationResponseMapping;\n when?: ConditionalExpression;\n}\n\ntype ValidatorConfig =\n | BuiltInValidatorConfig\n | CustomValidatorConfig\n | AsyncValidatorConfig\n | FunctionHttpValidatorConfig\n | DeclarativeHttpValidatorConfig;\n```\n\n> **Deprecation notes:**\n>\n> - `type: 'customAsync'` is deprecated. Use `type: 'async'` instead.\n> - `type: 'customHttp'` is deprecated. Use `type: 'http'` instead.\n> - `FunctionHttpValidatorConfig` and `DeclarativeHttpValidatorConfig` both use `type: 'http'`. They are discriminated by property presence: `functionName` indicates function-based, `http` + `responseMapping` indicates declarative.\n\n### HttpRequestConfig\n\nUsed by `DeclarativeHttpValidatorConfig` to define the HTTP request:\n\n```typescript\ninterface HttpRequestConfig {\n url: string;\n method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; // defaults to 'GET'\n queryParams?: Record<string, string>; // values are expressions\n body?: Record<string, unknown>;\n evaluateBodyExpressions?: boolean; // when true, top-level string values in body are evaluated as expressions\n headers?: Record<string, string>;\n}\n```\n\n### HttpValidationResponseMapping\n\nUsed by `DeclarativeHttpValidatorConfig` to interpret the HTTP response:\n\n```typescript\ninterface HttpValidationResponseMapping {\n validWhen: string; // expression evaluated with { response } scope; truthy = valid\n errorKind: string; // error kind for validationMessages lookup\n errorParams?: Record<string, string>; // parameter expressions evaluated against { response }\n}\n```\n\n## ConditionalExpression Types\n\n`ConditionalExpression` is a discriminated union of six condition types. Each variant only allows the properties relevant to its type, providing compile-time safety against invalid property combinations.\n\n```typescript\n// Compare a specific field's value\ninterface FieldValueCondition {\n type: 'fieldValue';\n fieldPath: string;\n operator: ComparisonOperator;\n value?: unknown;\n}\n\n// Compare the entire form value (deprecated \u2014 use FieldValueCondition or JavascriptCondition instead)\ninterface FormValueCondition {\n type: 'formValue';\n operator: ComparisonOperator;\n value?: unknown;\n}\n\n// Invoke a registered custom function by name\ninterface CustomCondition {\n type: 'custom';\n expression: string; // registered function name\n}\n\n// Evaluate a JavaScript expression via the secure AST-based parser\ninterface JavascriptCondition {\n type: 'javascript';\n expression: string; // has access to formValue, fieldValue, externalData, etc.\n}\n\n// Logical AND \u2014 all sub-conditions must be true\ninterface AndCondition {\n type: 'and';\n conditions: ConditionalExpression[];\n}\n\n// Logical OR \u2014 at least one sub-condition must be true\ninterface OrCondition {\n type: 'or';\n conditions: ConditionalExpression[];\n}\n\ntype ComparisonOperator =\n | 'equals'\n | 'notEquals'\n | 'greater'\n | 'less'\n | 'greaterOrEqual'\n | 'lessOrEqual'\n | 'contains'\n | 'startsWith'\n | 'endsWith'\n | 'matches';\n\ntype ConditionalExpression = FieldValueCondition | FormValueCondition | CustomCondition | JavascriptCondition | AndCondition | OrCondition;\n```\n\n> **Deprecation note:** `FormValueCondition` (`type: 'formValue'`) is deprecated. Use `FieldValueCondition` for specific field checks, or `JavascriptCondition` with an expression for complex form-level comparisons.\n\n## Validation Messages\n\n### Per-Field Messages\n\n```typescript\n{\n key: 'email',\n type: 'input',\n value: '',\n required: true,\n email: true,\n validationMessages: {\n required: 'Email is required',\n email: 'Invalid email format',\n },\n}\n```\n\n### Dynamic Messages (i18n)\n\n```typescript\n{\n key: 'email',\n type: 'input',\n value: '',\n required: true,\n validationMessages: {\n required: this.transloco.selectTranslate('validation.required'),\n email: this.transloco.selectTranslate('validation.email'),\n },\n}\n```\n\n## Common Patterns\n\n### Email Validation\n\n```typescript\n{\n key: 'email',\n type: 'input',\n value: '',\n required: true,\n email: true,\n validationMessages: {\n required: 'Email is required',\n email: 'Please enter a valid email address',\n },\n}\n```\n\n### Password Requirements\n\n```typescript\n{\n key: 'password',\n type: 'input',\n value: '',\n required: true,\n minLength: 8,\n maxLength: 128,\n pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\\\d)(?=.*[@$!%*?&])',\n validationMessages: {\n required: 'Password is required',\n minLength: 'Password must be at least 8 characters',\n pattern: 'Password must include uppercase, lowercase, number, and special character',\n },\n props: { type: 'password' },\n}\n```\n\n### Numeric Range\n\n```typescript\n{\n key: 'age',\n type: 'input',\n value: null,\n required: true,\n min: 18,\n max: 120,\n validationMessages: {\n required: 'Age is required',\n min: 'Must be at least 18 years old',\n max: 'Please enter a valid age',\n },\n props: { type: 'number' },\n}\n```\n\n### ZIP Code\n\n```typescript\n{\n key: 'zipCode',\n type: 'input',\n value: '',\n required: true,\n pattern: '^[0-9]{5}(-[0-9]{4})?$',\n validationMessages: {\n required: 'ZIP code is required',\n pattern: 'Must be 5 digits or 5+4 format (e.g., 12345 or 12345-6789)',\n },\n}\n```\n\n### Phone Number\n\n```typescript\n{\n key: 'phone',\n type: 'input',\n value: '',\n pattern: '^\\\\+?[1-9]\\\\d{1,14}$',\n validationMessages: {\n pattern: 'Please enter a valid phone number (E.164 format preferred)',\n },\n props: { type: 'tel' },\n}\n```\n\n### URL\n\n```typescript\n{\n key: 'website',\n type: 'input',\n value: '',\n pattern: '^https?:\\\\/\\\\/.+',\n validationMessages: {\n pattern: 'Please enter a valid URL starting with http:// or https://',\n },\n props: { type: 'url' },\n}\n```\n\n## Related\n\n- **[Validation Basics](../basics/)** - Getting started with validation\n- **[Validation Advanced](../advanced/)** - Conditional validators\n- **[Conditional Logic](../../dynamic-behavior/conditional-logic/overview/)** - Field behavior changes\n- **[Type Safety](../../advanced/type-safety/basics/)** - TypeScript integration"
273
- }
274
- ];
275
- }
276
- });
277
-
278
1
  // packages/dynamic-form-mcp/src/server.ts
279
2
  import { createRequire } from "module";
280
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
281
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
282
5
 
283
- // packages/dynamic-form-mcp/src/registry/field-types.json
284
- var field_types_default = [
6
+ // packages/dynamic-form-mcp/src/registry/field-types.ts
7
+ var FIELD_TYPES = [
285
8
  {
286
9
  type: "input",
287
10
  category: "value",
@@ -306,8 +29,15 @@ var field_types_default = [
306
29
  validationSupported: true,
307
30
  source: "adapter",
308
31
  allowedIn: ["top-level", "page", "row", "group", "array"],
309
- example: "{\n key: 'email',\n type: 'input',\n label: 'Email Address',\n required: true,\n email: true,\n props: { type: 'email', placeholder: 'user@example.com' }\n}",
310
- minimalExample: "{ key: 'name', type: 'input', label: 'Name' }"
32
+ example: `{
33
+ key: 'email',
34
+ type: 'input',
35
+ label: 'Email Address',
36
+ required: true,
37
+ email: true,
38
+ props: { type: 'email', placeholder: 'user@example.com' }
39
+ }`,
40
+ minimalExample: `{ key: 'name', type: 'input', label: 'Name' }`
311
41
  },
312
42
  {
313
43
  type: "textarea",
@@ -333,8 +63,13 @@ var field_types_default = [
333
63
  validationSupported: true,
334
64
  source: "adapter",
335
65
  allowedIn: ["top-level", "page", "row", "group", "array"],
336
- example: "{\n key: 'description',\n type: 'textarea',\n label: 'Description',\n props: { rows: 5, placeholder: 'Enter description...' }\n}",
337
- minimalExample: "{ key: 'bio', type: 'textarea', label: 'Bio' }"
66
+ example: `{
67
+ key: 'description',
68
+ type: 'textarea',
69
+ label: 'Description',
70
+ props: { rows: 5, placeholder: 'Enter description...' }
71
+ }`,
72
+ minimalExample: `{ key: 'bio', type: 'textarea', label: 'Bio' }`
338
73
  },
339
74
  {
340
75
  type: "select",
@@ -366,8 +101,17 @@ var field_types_default = [
366
101
  validationSupported: true,
367
102
  source: "adapter",
368
103
  allowedIn: ["top-level", "page", "row", "group", "array"],
369
- example: "{\n key: 'country',\n type: 'select',\n label: 'Country',\n required: true,\n options: [\n { label: 'United States', value: 'us' },\n { label: 'Canada', value: 'ca' }\n ]\n}",
370
- minimalExample: "{ key: 'status', type: 'select', label: 'Status', options: [{ label: 'Active', value: 'active' }] }"
104
+ example: `{
105
+ key: 'country',
106
+ type: 'select',
107
+ label: 'Country',
108
+ required: true,
109
+ options: [
110
+ { label: 'United States', value: 'us' },
111
+ { label: 'Canada', value: 'ca' }
112
+ ]
113
+ }`,
114
+ minimalExample: `{ key: 'status', type: 'select', label: 'Status', options: [{ label: 'Active', value: 'active' }] }`
371
115
  },
372
116
  {
373
117
  type: "checkbox",
@@ -379,8 +123,13 @@ var field_types_default = [
379
123
  validationSupported: true,
380
124
  source: "adapter",
381
125
  allowedIn: ["top-level", "page", "row", "group", "array"],
382
- example: "{\n key: 'acceptTerms',\n type: 'checkbox',\n label: 'I accept the terms and conditions',\n required: true\n}",
383
- minimalExample: "{ key: 'agree', type: 'checkbox', label: 'I agree' }"
126
+ example: `{
127
+ key: 'acceptTerms',
128
+ type: 'checkbox',
129
+ label: 'I accept the terms and conditions',
130
+ required: true
131
+ }`,
132
+ minimalExample: `{ key: 'agree', type: 'checkbox', label: 'I agree' }`
384
133
  },
385
134
  {
386
135
  type: "multi-checkbox",
@@ -399,8 +148,18 @@ var field_types_default = [
399
148
  validationSupported: true,
400
149
  source: "adapter",
401
150
  allowedIn: ["top-level", "page", "row", "group", "array"],
402
- example: "{\n key: 'interests',\n type: 'multi-checkbox',\n label: 'Select your interests',\n options: [\n { label: 'Sports', value: 'sports' },\n { label: 'Music', value: 'music' },\n { label: 'Art', value: 'art' },\n { label: 'Technology', value: 'tech' }\n ]\n}",
403
- minimalExample: "{ key: 'tags', type: 'multi-checkbox', label: 'Tags', options: [{ label: 'A', value: 'a' }] }"
151
+ example: `{
152
+ key: 'interests',
153
+ type: 'multi-checkbox',
154
+ label: 'Select your interests',
155
+ options: [
156
+ { label: 'Sports', value: 'sports' },
157
+ { label: 'Music', value: 'music' },
158
+ { label: 'Art', value: 'art' },
159
+ { label: 'Technology', value: 'tech' }
160
+ ]
161
+ }`,
162
+ minimalExample: `{ key: 'tags', type: 'multi-checkbox', label: 'Tags', options: [{ label: 'A', value: 'a' }] }`
404
163
  },
405
164
  {
406
165
  type: "radio",
@@ -419,8 +178,17 @@ var field_types_default = [
419
178
  validationSupported: true,
420
179
  source: "adapter",
421
180
  allowedIn: ["top-level", "page", "row", "group", "array"],
422
- example: "{\n key: 'gender',\n type: 'radio',\n label: 'Gender',\n options: [\n { label: 'Male', value: 'male' },\n { label: 'Female', value: 'female' },\n { label: 'Other', value: 'other' }\n ]\n}",
423
- minimalExample: "{ key: 'choice', type: 'radio', label: 'Choice', options: [{ label: 'Yes', value: 'yes' }, { label: 'No', value: 'no' }] }"
181
+ example: `{
182
+ key: 'gender',
183
+ type: 'radio',
184
+ label: 'Gender',
185
+ options: [
186
+ { label: 'Male', value: 'male' },
187
+ { label: 'Female', value: 'female' },
188
+ { label: 'Other', value: 'other' }
189
+ ]
190
+ }`,
191
+ minimalExample: `{ key: 'choice', type: 'radio', label: 'Choice', options: [{ label: 'Yes', value: 'yes' }, { label: 'No', value: 'no' }] }`
424
192
  },
425
193
  {
426
194
  type: "datepicker",
@@ -445,8 +213,14 @@ var field_types_default = [
445
213
  validationSupported: true,
446
214
  source: "adapter",
447
215
  allowedIn: ["top-level", "page", "row", "group", "array"],
448
- example: "{\n key: 'birthDate',\n type: 'datepicker',\n label: 'Date of Birth',\n required: true,\n props: { maxDate: new Date() }\n}",
449
- minimalExample: "{ key: 'date', type: 'datepicker', label: 'Date' }"
216
+ example: `{
217
+ key: 'birthDate',
218
+ type: 'datepicker',
219
+ label: 'Date of Birth',
220
+ required: true,
221
+ props: { maxDate: new Date() }
222
+ }`,
223
+ minimalExample: `{ key: 'date', type: 'datepicker', label: 'Date' }`
450
224
  },
451
225
  {
452
226
  type: "toggle",
@@ -458,8 +232,12 @@ var field_types_default = [
458
232
  validationSupported: true,
459
233
  source: "adapter",
460
234
  allowedIn: ["top-level", "page", "row", "group", "array"],
461
- example: "{\n key: 'notifications',\n type: 'toggle',\n label: 'Enable notifications'\n}",
462
- minimalExample: "{ key: 'enabled', type: 'toggle', label: 'Enable' }"
235
+ example: `{
236
+ key: 'notifications',
237
+ type: 'toggle',
238
+ label: 'Enable notifications'
239
+ }`,
240
+ minimalExample: `{ key: 'enabled', type: 'toggle', label: 'Enable' }`
463
241
  },
464
242
  {
465
243
  type: "slider",
@@ -493,8 +271,16 @@ var field_types_default = [
493
271
  validationSupported: true,
494
272
  source: "adapter",
495
273
  allowedIn: ["top-level", "page", "row", "group", "array"],
496
- example: "{\n key: 'volume',\n type: 'slider',\n label: 'Volume',\n value: 50,\n minValue: 0,\n maxValue: 100,\n step: 5\n}",
497
- minimalExample: "{ key: 'rating', type: 'slider', label: 'Rating' }"
274
+ example: `{
275
+ key: 'volume',
276
+ type: 'slider',
277
+ label: 'Volume',
278
+ value: 50,
279
+ minValue: 0,
280
+ maxValue: 100,
281
+ step: 5
282
+ }`,
283
+ minimalExample: `{ key: 'rating', type: 'slider', label: 'Rating' }`
498
284
  },
499
285
  {
500
286
  type: "hidden",
@@ -507,13 +293,29 @@ var field_types_default = [
507
293
  source: "core",
508
294
  allowedIn: ["top-level (single-page forms only)", "page.fields", "group.fields", "array.fields"],
509
295
  notAllowedIn: ["row", "top-level when using pages"],
510
- example: "// Hidden field - value is REQUIRED!\n{\n key: 'userId',\n type: 'hidden',\n value: 'abc123' // REQUIRED: string, number, boolean, or array\n}\n\n// Valid value types:\n{ key: 'count', type: 'hidden', value: 42 } // number\n{ key: 'active', type: 'hidden', value: true } // boolean\n{ key: 'tags', type: 'hidden', value: [1, 2, 3] } // array\n\n// \u274C FORBIDDEN (will cause validation error):\n// - label, logic, validators, required, props\n// - disabled, readonly, hidden, col, tabIndex, meta\n// - Validation shorthand: email, min, max, minLength, maxLength, pattern",
511
- minimalExample: "{ key: 'id', type: 'hidden', value: 'abc123' }"
296
+ example: `// Hidden field - value is REQUIRED!
297
+ {
298
+ key: 'userId',
299
+ type: 'hidden',
300
+ value: 'abc123' // REQUIRED: string, number, boolean, or array
301
+ }
302
+
303
+ // Valid value types:
304
+ { key: 'count', type: 'hidden', value: 42 } // number
305
+ { key: 'active', type: 'hidden', value: true } // boolean
306
+ { key: 'tags', type: 'hidden', value: [1, 2, 3] } // array
307
+
308
+ // \u274C FORBIDDEN (will cause validation error):
309
+ // - label, logic, validators, required, props
310
+ // - disabled, readonly, hidden, col, tabIndex, meta
311
+ // - Validation shorthand: email, min, max, minLength, maxLength, pattern`,
312
+ minimalExample: `{ key: 'id', type: 'hidden', value: 'abc123' }`
512
313
  },
513
314
  {
514
315
  type: "text",
515
316
  category: "display",
516
317
  description: "Static text display element (headings, paragraphs, spans). The text content comes from the label property.",
318
+ valueType: void 0,
517
319
  baseInterface: "FieldDef",
518
320
  props: {
519
321
  elementType: {
@@ -527,13 +329,19 @@ var field_types_default = [
527
329
  validationSupported: false,
528
330
  source: "core",
529
331
  allowedIn: ["top-level", "page", "row", "group", "array"],
530
- example: "{\n key: 'sectionTitle',\n type: 'text',\n label: 'Personal Information',\n props: { elementType: 'h2' }\n}",
531
- minimalExample: "{ key: 'header', type: 'text', label: 'Section Title' }"
332
+ example: `{
333
+ key: 'sectionTitle',
334
+ type: 'text',
335
+ label: 'Personal Information',
336
+ props: { elementType: 'h2' }
337
+ }`,
338
+ minimalExample: `{ key: 'header', type: 'text', label: 'Section Title' }`
532
339
  },
533
340
  {
534
341
  type: "row",
535
342
  category: "container",
536
343
  description: "Horizontal layout container for grouping fields in columns. Rows do NOT have a label property. Supports only 'hidden' logic type for conditional visibility.",
344
+ valueType: void 0,
537
345
  baseInterface: "FieldDef",
538
346
  props: {
539
347
  fields: {
@@ -566,8 +374,21 @@ var field_types_default = [
566
374
  "previous"
567
375
  ],
568
376
  cannotContain: ["page", "row", "hidden"],
569
- example: "{\n key: 'nameRow',\n type: 'row',\n // NOTE: Rows do NOT have a label property\n // Rows support only 'hidden' logic type for conditional visibility\n fields: [\n { key: 'firstName', type: 'input', label: 'First Name', col: 6 },\n { key: 'lastName', type: 'input', label: 'Last Name', col: 6 }\n ]\n}\n// NOT ALLOWED in rows:\n// - type: 'hidden' (hidden fields cannot be inside rows)\n// - type: 'page' (pages are top-level only)\n// - type: 'row' (no nested rows)",
570
- minimalExample: "{ key: 'row1', type: 'row', fields: [...] }"
377
+ example: `{
378
+ key: 'nameRow',
379
+ type: 'row',
380
+ // NOTE: Rows do NOT have a label property
381
+ // Rows support only 'hidden' logic type for conditional visibility
382
+ fields: [
383
+ { key: 'firstName', type: 'input', label: 'First Name', col: 6 },
384
+ { key: 'lastName', type: 'input', label: 'Last Name', col: 6 }
385
+ ]
386
+ }
387
+ // NOT ALLOWED in rows:
388
+ // - type: 'hidden' (hidden fields cannot be inside rows)
389
+ // - type: 'page' (pages are top-level only)
390
+ // - type: 'row' (no nested rows)`,
391
+ minimalExample: `{ key: 'row1', type: 'row', fields: [...] }`
571
392
  },
572
393
  {
573
394
  type: "group",
@@ -606,8 +427,17 @@ var field_types_default = [
606
427
  "previous"
607
428
  ],
608
429
  cannotContain: ["page", "group"],
609
- example: "{\n key: 'address',\n type: 'group',\n // NOTE: Groups do NOT have a label property\n fields: [\n { key: 'street', type: 'input', label: 'Street' },\n { key: 'city', type: 'input', label: 'City' },\n { key: 'zip', type: 'input', label: 'ZIP Code' }\n ]\n}",
610
- minimalExample: "{ key: 'address', type: 'group', fields: [...] }"
430
+ example: `{
431
+ key: 'address',
432
+ type: 'group',
433
+ // NOTE: Groups do NOT have a label property
434
+ fields: [
435
+ { key: 'street', type: 'input', label: 'Street' },
436
+ { key: 'city', type: 'input', label: 'City' },
437
+ { key: 'zip', type: 'input', label: 'ZIP Code' }
438
+ ]
439
+ }`,
440
+ minimalExample: `{ key: 'address', type: 'group', fields: [...] }`
611
441
  },
612
442
  {
613
443
  type: "array",
@@ -659,13 +489,39 @@ var field_types_default = [
659
489
  "previous"
660
490
  ],
661
491
  cannotContain: ["page", "array"],
662
- example: "// Flat array with length validation\n{\n key: 'tags',\n type: 'array',\n minLength: 1, // At least one tag required\n maxLength: 5, // No more than 5 tags\n fields: [\n { key: 'tag', type: 'input', label: 'Tag' }\n ]\n}\n// Result: { tags: ['value1', 'value2'] }\n\n// Object array (nested groups)\n{\n key: 'contacts',\n type: 'array',\n minLength: 1,\n fields: [{\n type: 'group',\n fields: [\n { key: 'name', type: 'input', label: 'Name' },\n { key: 'email', type: 'input', label: 'Email' }\n ]\n }]\n}\n// Result: { contacts: [{name: '', email: ''}, ...] }",
663
- minimalExample: "{ key: 'items', type: 'array', fields: [...] }"
492
+ example: `// Flat array with length validation
493
+ {
494
+ key: 'tags',
495
+ type: 'array',
496
+ minLength: 1, // At least one tag required
497
+ maxLength: 5, // No more than 5 tags
498
+ fields: [
499
+ { key: 'tag', type: 'input', label: 'Tag' }
500
+ ]
501
+ }
502
+ // Result: { tags: ['value1', 'value2'] }
503
+
504
+ // Object array (nested groups)
505
+ {
506
+ key: 'contacts',
507
+ type: 'array',
508
+ minLength: 1,
509
+ fields: [{
510
+ type: 'group',
511
+ fields: [
512
+ { key: 'name', type: 'input', label: 'Name' },
513
+ { key: 'email', type: 'input', label: 'Email' }
514
+ ]
515
+ }]
516
+ }
517
+ // Result: { contacts: [{name: '', email: ''}, ...] }`,
518
+ minimalExample: `{ key: 'items', type: 'array', fields: [...] }`
664
519
  },
665
520
  {
666
521
  type: "page",
667
522
  category: "container",
668
523
  description: "Multi-step form page container for wizard-style forms. Pages do NOT have label or title properties. ALL top-level fields must be pages if using multi-page mode. IMPORTANT: Navigation buttons (next, previous) must be added as fields WITHIN each page that needs them.",
524
+ valueType: void 0,
669
525
  baseInterface: "FieldDef",
670
526
  props: {
671
527
  fields: {
@@ -700,37 +556,58 @@ var field_types_default = [
700
556
  "previous"
701
557
  ],
702
558
  cannotContain: ["page"],
703
- example: "{\n key: 'step1',\n type: 'page',\n // NOTE: Pages do NOT have label or title properties\n // Navigation buttons are fields WITHIN the page\n fields: [\n { key: 'firstName', type: 'input', label: 'First Name' },\n { key: 'lastName', type: 'input', label: 'Last Name' },\n { key: 'next', type: 'next', label: 'Next' }\n ]\n}",
704
- minimalExample: "{ key: 'step1', type: 'page', fields: [...] }"
559
+ example: `{
560
+ key: 'step1',
561
+ type: 'page',
562
+ // NOTE: Pages do NOT have label or title properties
563
+ // Navigation buttons are fields WITHIN the page
564
+ fields: [
565
+ { key: 'firstName', type: 'input', label: 'First Name' },
566
+ { key: 'lastName', type: 'input', label: 'Last Name' },
567
+ { key: 'next', type: 'next', label: 'Next' }
568
+ ]
569
+ }`,
570
+ minimalExample: `{ key: 'step1', type: 'page', fields: [...] }`
705
571
  },
706
572
  {
707
573
  type: "next",
708
574
  category: "button",
709
575
  description: "Navigation button to go to next page in multi-step forms. Must be placed INSIDE a page's fields array.",
576
+ valueType: void 0,
710
577
  baseInterface: "FieldDef",
711
578
  props: {},
712
579
  validationSupported: false,
713
580
  source: "adapter",
714
581
  allowedIn: ["top-level", "page", "row", "group", "array"],
715
- example: "{\n key: 'next',\n type: 'next',\n label: 'Next'\n}",
716
- minimalExample: "{ key: 'next', type: 'next', label: 'Next' }"
582
+ example: `{
583
+ key: 'next',
584
+ type: 'next',
585
+ label: 'Next'
586
+ }`,
587
+ minimalExample: `{ key: 'next', type: 'next', label: 'Next' }`
717
588
  },
718
589
  {
719
590
  type: "previous",
720
591
  category: "button",
721
592
  description: "Navigation button to go to previous page in multi-step forms. Must be placed INSIDE a page's fields array.",
593
+ valueType: void 0,
722
594
  baseInterface: "FieldDef",
723
595
  props: {},
724
596
  validationSupported: false,
725
597
  source: "adapter",
726
598
  allowedIn: ["top-level", "page", "row", "group", "array"],
727
- example: "{\n key: 'back',\n type: 'previous',\n label: 'Back'\n}",
728
- minimalExample: "{ key: 'back', type: 'previous', label: 'Back' }"
599
+ example: `{
600
+ key: 'back',
601
+ type: 'previous',
602
+ label: 'Back'
603
+ }`,
604
+ minimalExample: `{ key: 'back', type: 'previous', label: 'Back' }`
729
605
  },
730
606
  {
731
607
  type: "button",
732
608
  category: "button",
733
609
  description: "Generic button for custom actions. IMPORTANT: The generic 'button' type REQUIRES the 'event' property with a FormEventConstructor. For simple use cases, prefer 'submit', 'next', or 'previous' which don't require event configuration.",
610
+ valueType: void 0,
734
611
  baseInterface: "FieldDef",
735
612
  props: {
736
613
  event: {
@@ -743,62 +620,93 @@ var field_types_default = [
743
620
  validationSupported: false,
744
621
  source: "adapter",
745
622
  allowedIn: ["top-level", "page", "row", "group", "array"],
746
- example: "// RECOMMENDED: Use pre-defined button types for common actions\n{ key: 'submit', type: 'submit', label: 'Submit Form' }\n{ key: 'next', type: 'next', label: 'Next' }\n{ key: 'back', type: 'previous', label: 'Back' }\n\n// ADVANCED: Generic button with custom event (requires setup)\n// 1. Create event class: class MyCustomEvent extends FormEvent {}\n// 2. Register with: provideFormEvents([MyCustomEvent])\n// 3. Use in config:\n{\n key: 'customAction',\n type: 'button',\n label: 'Custom Action',\n event: MyCustomEvent // Must be the class reference\n}",
747
- minimalExample: "{ key: 'action', type: 'button', label: 'Action', event: MyEvent }"
623
+ example: `// RECOMMENDED: Use pre-defined button types for common actions
624
+ { key: 'submit', type: 'submit', label: 'Submit Form' }
625
+ { key: 'next', type: 'next', label: 'Next' }
626
+ { key: 'back', type: 'previous', label: 'Back' }
627
+
628
+ // ADVANCED: Generic button with custom event (requires setup)
629
+ // 1. Create event class: class MyCustomEvent extends FormEvent {}
630
+ // 2. Register with: provideFormEvents([MyCustomEvent])
631
+ // 3. Use in config:
632
+ {
633
+ key: 'customAction',
634
+ type: 'button',
635
+ label: 'Custom Action',
636
+ event: MyCustomEvent // Must be the class reference
637
+ }`,
638
+ minimalExample: `{ key: 'action', type: 'button', label: 'Action', event: MyEvent }`
748
639
  },
749
640
  {
750
641
  type: "submit",
751
642
  category: "button",
752
643
  description: "Form submit button",
644
+ valueType: void 0,
753
645
  baseInterface: "FieldDef",
754
646
  props: {},
755
647
  validationSupported: false,
756
648
  source: "adapter",
757
649
  allowedIn: ["top-level", "page", "row", "group", "array"],
758
- example: "{\n key: 'submit',\n type: 'submit',\n label: 'Submit Form'\n}",
759
- minimalExample: "{ key: 'submit', type: 'submit', label: 'Submit' }"
650
+ example: `{
651
+ key: 'submit',
652
+ type: 'submit',
653
+ label: 'Submit Form'
654
+ }`,
655
+ minimalExample: `{ key: 'submit', type: 'submit', label: 'Submit' }`
760
656
  },
761
657
  {
762
658
  type: "addArrayItem",
763
659
  category: "button",
764
660
  description: "Button to add a new item to an array field. Must be placed within or near the array container.",
661
+ valueType: void 0,
765
662
  baseInterface: "FieldDef",
766
663
  props: {},
767
664
  validationSupported: false,
768
665
  source: "adapter",
769
666
  allowedIn: ["array", "row", "group"],
770
- example: "{\n key: 'addContact',\n type: 'addArrayItem',\n label: 'Add Contact'\n}",
771
- minimalExample: "{ key: 'add', type: 'addArrayItem', label: 'Add Item' }"
667
+ example: `{
668
+ key: 'addContact',
669
+ type: 'addArrayItem',
670
+ label: 'Add Contact'
671
+ }`,
672
+ minimalExample: `{ key: 'add', type: 'addArrayItem', label: 'Add Item' }`
772
673
  },
773
674
  {
774
675
  type: "removeArrayItem",
775
676
  category: "button",
776
677
  description: "Button to remove the current item from an array field. Typically placed within each array item template.",
678
+ valueType: void 0,
777
679
  baseInterface: "FieldDef",
778
680
  props: {},
779
681
  validationSupported: false,
780
682
  source: "adapter",
781
683
  allowedIn: ["array", "row", "group"],
782
- example: "{\n key: 'removeContact',\n type: 'removeArrayItem',\n label: 'Remove'\n}",
783
- minimalExample: "{ key: 'remove', type: 'removeArrayItem', label: 'Remove' }"
684
+ example: `{
685
+ key: 'removeContact',
686
+ type: 'removeArrayItem',
687
+ label: 'Remove'
688
+ }`,
689
+ minimalExample: `{ key: 'remove', type: 'removeArrayItem', label: 'Remove' }`
784
690
  }
785
691
  ];
786
692
 
787
- // packages/dynamic-form-mcp/src/registry/validators.json
788
- var validators_default = [
693
+ // packages/dynamic-form-mcp/src/registry/validators.ts
694
+ var VALIDATORS = [
789
695
  {
790
696
  type: "required",
791
697
  category: "built-in",
792
698
  description: "Validates that a field has a non-empty value",
793
699
  parameters: {},
794
- example: "{ type: 'required' }\n// or shorthand: required: true"
700
+ example: `{ type: 'required' }
701
+ // or shorthand: required: true`
795
702
  },
796
703
  {
797
704
  type: "email",
798
705
  category: "built-in",
799
706
  description: "Validates email format",
800
707
  parameters: {},
801
- example: "{ type: 'email' }\n// or shorthand: email: true"
708
+ example: `{ type: 'email' }
709
+ // or shorthand: email: true`
802
710
  },
803
711
  {
804
712
  type: "min",
@@ -812,7 +720,8 @@ var validators_default = [
812
720
  required: true
813
721
  }
814
722
  },
815
- example: "{ type: 'min', value: 0 }\n// or shorthand: min: 0"
723
+ example: `{ type: 'min', value: 0 }
724
+ // or shorthand: min: 0`
816
725
  },
817
726
  {
818
727
  type: "max",
@@ -826,7 +735,8 @@ var validators_default = [
826
735
  required: true
827
736
  }
828
737
  },
829
- example: "{ type: 'max', value: 100 }\n// or shorthand: max: 100"
738
+ example: `{ type: 'max', value: 100 }
739
+ // or shorthand: max: 100`
830
740
  },
831
741
  {
832
742
  type: "minLength",
@@ -840,7 +750,8 @@ var validators_default = [
840
750
  required: true
841
751
  }
842
752
  },
843
- example: "{ type: 'minLength', value: 3 }\n// or shorthand: minLength: 3"
753
+ example: `{ type: 'minLength', value: 3 }
754
+ // or shorthand: minLength: 3`
844
755
  },
845
756
  {
846
757
  type: "maxLength",
@@ -854,7 +765,8 @@ var validators_default = [
854
765
  required: true
855
766
  }
856
767
  },
857
- example: "{ type: 'maxLength', value: 100 }\n// or shorthand: maxLength: 100"
768
+ example: `{ type: 'maxLength', value: 100 }
769
+ // or shorthand: maxLength: 100`
858
770
  },
859
771
  {
860
772
  type: "pattern",
@@ -868,7 +780,8 @@ var validators_default = [
868
780
  required: true
869
781
  }
870
782
  },
871
- example: "{ type: 'pattern', value: '^[A-Z][a-z]+$' }\n// or shorthand: pattern: '^[A-Z][a-z]+$'"
783
+ example: `{ type: 'pattern', value: '^[A-Z][a-z]+$' }
784
+ // or shorthand: pattern: '^[A-Z][a-z]+$'`
872
785
  },
873
786
  {
874
787
  type: "custom",
@@ -900,7 +813,27 @@ var validators_default = [
900
813
  required: false
901
814
  }
902
815
  },
903
- example: "// Expression-based custom validator with error message\n// The validator:\n{\n type: 'custom',\n expression: 'fieldValue === formValue.password',\n kind: 'passwordMismatch' // Just the error KIND, not the message\n}\n\n// COMPLETE FIELD with validationMessages at FIELD level:\n{\n key: 'confirmPassword',\n type: 'input',\n label: 'Confirm Password',\n props: { type: 'password' },\n validators: [\n { type: 'custom', expression: 'fieldValue === formValue.password', kind: 'passwordMismatch' }\n ],\n validationMessages: {\n passwordMismatch: 'Passwords do not match' // Message goes HERE at field level\n }\n}"
816
+ example: `// Expression-based custom validator with error message
817
+ // The validator:
818
+ {
819
+ type: 'custom',
820
+ expression: 'fieldValue === formValue.password',
821
+ kind: 'passwordMismatch' // Just the error KIND, not the message
822
+ }
823
+
824
+ // COMPLETE FIELD with validationMessages at FIELD level:
825
+ {
826
+ key: 'confirmPassword',
827
+ type: 'input',
828
+ label: 'Confirm Password',
829
+ props: { type: 'password' },
830
+ validators: [
831
+ { type: 'custom', expression: 'fieldValue === formValue.password', kind: 'passwordMismatch' }
832
+ ],
833
+ validationMessages: {
834
+ passwordMismatch: 'Passwords do not match' // Message goes HERE at field level
835
+ }
836
+ }`
904
837
  },
905
838
  {
906
839
  type: "async",
@@ -920,7 +853,10 @@ var validators_default = [
920
853
  required: false
921
854
  }
922
855
  },
923
- example: "{\n type: 'async',\n functionName: 'checkUsernameAvailability'\n}"
856
+ example: `{
857
+ type: 'async',
858
+ functionName: 'checkUsernameAvailability'
859
+ }`
924
860
  },
925
861
  {
926
862
  type: "customAsync",
@@ -940,7 +876,11 @@ var validators_default = [
940
876
  required: false
941
877
  }
942
878
  },
943
- example: "// DEPRECATED \u2014 use type: 'async' instead\n{\n type: 'customAsync',\n functionName: 'checkUsernameAvailability'\n}"
879
+ example: `// DEPRECATED \u2014 use type: 'async' instead
880
+ {
881
+ type: 'customAsync',
882
+ functionName: 'checkUsernameAvailability'
883
+ }`
944
884
  },
945
885
  {
946
886
  type: "customHttp",
@@ -960,7 +900,11 @@ var validators_default = [
960
900
  required: false
961
901
  }
962
902
  },
963
- example: "// DEPRECATED \u2014 use type: 'http' with functionName instead\n{\n type: 'customHttp',\n functionName: 'validateEmailExists'\n}"
903
+ example: `// DEPRECATED \u2014 use type: 'http' with functionName instead
904
+ {
905
+ type: 'customHttp',
906
+ functionName: 'validateEmailExists'
907
+ }`
964
908
  },
965
909
  {
966
910
  type: "http",
@@ -992,12 +936,54 @@ var validators_default = [
992
936
  required: false
993
937
  }
994
938
  },
995
- example: "// HTTP validator (fully JSON-serializable)\n{\n type: 'http',\n http: {\n url: '/api/validate-username',\n method: 'GET',\n queryParams: {\n username: 'fieldValue'\n }\n },\n responseMapping: {\n validWhen: 'response.available',\n errorKind: 'usernameTaken',\n errorParams: {\n suggestion: 'response.suggestion'\n }\n }\n}\n\n// COMPLETE FIELD with validationMessages at FIELD level:\n{\n key: 'username',\n type: 'input',\n label: 'Username',\n validators: [\n {\n type: 'http',\n http: {\n url: '/api/validate-username',\n method: 'GET',\n queryParams: { username: 'fieldValue' }\n },\n responseMapping: {\n validWhen: 'response.available',\n errorKind: 'usernameTaken',\n errorParams: { suggestion: 'response.suggestion' }\n }\n }\n ],\n validationMessages: {\n usernameTaken: 'Username is taken. Try {{suggestion}}'\n }\n}"
939
+ example: `// HTTP validator (fully JSON-serializable)
940
+ {
941
+ type: 'http',
942
+ http: {
943
+ url: '/api/validate-username',
944
+ method: 'GET',
945
+ queryParams: {
946
+ username: 'fieldValue'
947
+ }
948
+ },
949
+ responseMapping: {
950
+ validWhen: 'response.available',
951
+ errorKind: 'usernameTaken',
952
+ errorParams: {
953
+ suggestion: 'response.suggestion'
954
+ }
955
+ }
956
+ }
957
+
958
+ // COMPLETE FIELD with validationMessages at FIELD level:
959
+ {
960
+ key: 'username',
961
+ type: 'input',
962
+ label: 'Username',
963
+ validators: [
964
+ {
965
+ type: 'http',
966
+ http: {
967
+ url: '/api/validate-username',
968
+ method: 'GET',
969
+ queryParams: { username: 'fieldValue' }
970
+ },
971
+ responseMapping: {
972
+ validWhen: 'response.available',
973
+ errorKind: 'usernameTaken',
974
+ errorParams: { suggestion: 'response.suggestion' }
975
+ }
976
+ }
977
+ ],
978
+ validationMessages: {
979
+ usernameTaken: 'Username is taken. Try {{suggestion}}'
980
+ }
981
+ }`
996
982
  }
997
983
  ];
998
984
 
999
- // packages/dynamic-form-mcp/src/registry/ui-adapters.json
1000
- var ui_adapters_default = [
985
+ // packages/dynamic-form-mcp/src/registry/ui-adapters.ts
986
+ var UI_ADAPTERS = [
1001
987
  {
1002
988
  library: "material",
1003
989
  package: "@ng-forge/dynamic-forms-material",
@@ -1223,66 +1209,165 @@ var ui_adapters_default = [
1223
1209
  }
1224
1210
  ];
1225
1211
 
1212
+ // packages/dynamic-form-mcp/src/registry/documentation.ts
1213
+ var BASE_URL = "https://ng-forge.com/dynamic-forms";
1214
+ var DOCUMENTATION = [
1215
+ // Getting Started
1216
+ { id: "installation", title: "Installation", category: "getting-started", url: `${BASE_URL}/installation` },
1217
+ // UI Library Integrations
1218
+ {
1219
+ id: "ui-libs-material",
1220
+ title: "Angular Material Integration",
1221
+ category: "ui-integrations",
1222
+ url: `${BASE_URL}/ui-libs-integrations/material`
1223
+ },
1224
+ { id: "ui-libs-primeng", title: "PrimeNG Integration", category: "ui-integrations", url: `${BASE_URL}/ui-libs-integrations/primeng` },
1225
+ { id: "ui-libs-ionic", title: "Ionic Integration", category: "ui-integrations", url: `${BASE_URL}/ui-libs-integrations/ionic` },
1226
+ {
1227
+ id: "ui-libs-bootstrap",
1228
+ title: "Bootstrap Integration",
1229
+ category: "ui-integrations",
1230
+ url: `${BASE_URL}/ui-libs-integrations/bootstrap`
1231
+ },
1232
+ // Field Types
1233
+ { id: "field-types", title: "Field Types", category: "field-types", url: `${BASE_URL}/field-types` },
1234
+ // Validation
1235
+ { id: "validation-basics", title: "Validation Basics", category: "validation", url: `${BASE_URL}/validation/basics` },
1236
+ { id: "validation-advanced", title: "Conditional Validators", category: "validation", url: `${BASE_URL}/validation/advanced` },
1237
+ {
1238
+ id: "validation-custom",
1239
+ title: "Custom & Expression Validators",
1240
+ category: "validation",
1241
+ url: `${BASE_URL}/validation/custom-validators`
1242
+ },
1243
+ { id: "validation-reference", title: "Validation Reference", category: "validation", url: `${BASE_URL}/validation/reference` },
1244
+ // Dynamic Behavior
1245
+ { id: "conditional-logic", title: "Conditional Logic", category: "dynamic-behavior", url: `${BASE_URL}/dynamic-behavior/overview` },
1246
+ { id: "value-derivation", title: "Value Derivation", category: "dynamic-behavior", url: `${BASE_URL}/dynamic-behavior/derivation` },
1247
+ {
1248
+ id: "property-derivation",
1249
+ title: "Property Derivation",
1250
+ category: "dynamic-behavior",
1251
+ url: `${BASE_URL}/dynamic-behavior/derivation/property`
1252
+ },
1253
+ { id: "i18n", title: "Internationalization", category: "dynamic-behavior", url: `${BASE_URL}/dynamic-behavior/i18n` },
1254
+ { id: "submission", title: "Form Submission", category: "dynamic-behavior", url: `${BASE_URL}/dynamic-behavior/submission` },
1255
+ // Schema Validation
1256
+ {
1257
+ id: "schema-overview",
1258
+ title: "Schema Validation Overview",
1259
+ category: "schema-validation",
1260
+ url: `${BASE_URL}/schema-validation/overview`
1261
+ },
1262
+ {
1263
+ id: "schema-angular",
1264
+ title: "Angular Native Schema",
1265
+ category: "schema-validation",
1266
+ url: `${BASE_URL}/schema-validation/angular-schema`
1267
+ },
1268
+ {
1269
+ id: "schema-zod",
1270
+ title: "Standard Schema (Zod, Valibot, ArkType)",
1271
+ category: "schema-validation",
1272
+ url: `${BASE_URL}/schema-validation/zod`
1273
+ },
1274
+ // Layout Components
1275
+ { id: "form-pages", title: "Page Containers", category: "layout", url: `${BASE_URL}/prebuilt/form-pages` },
1276
+ { id: "form-groups", title: "Field Grouping", category: "layout", url: `${BASE_URL}/prebuilt/form-groups` },
1277
+ { id: "form-rows", title: "Row Layout", category: "layout", url: `${BASE_URL}/prebuilt/form-rows` },
1278
+ { id: "form-arrays-simplified", title: "Simplified Array API", category: "layout", url: `${BASE_URL}/prebuilt/form-arrays/simplified` },
1279
+ { id: "form-arrays-complete", title: "Complete Array API", category: "layout", url: `${BASE_URL}/prebuilt/form-arrays/complete` },
1280
+ { id: "text-components", title: "Text & Label Components", category: "layout", url: `${BASE_URL}/prebuilt/text-components` },
1281
+ { id: "hidden-fields", title: "Hidden Fields", category: "layout", url: `${BASE_URL}/prebuilt/hidden-fields` },
1282
+ // Advanced
1283
+ { id: "type-safety", title: "Type Safety", category: "advanced", url: `${BASE_URL}/advanced/basics` },
1284
+ { id: "events", title: "Form & Field Events", category: "advanced", url: `${BASE_URL}/advanced/events` },
1285
+ { id: "expression-parser", title: "Expression Syntax", category: "advanced", url: `${BASE_URL}/advanced/expression-parser` },
1286
+ {
1287
+ id: "custom-integrations",
1288
+ title: "Custom UI Library Integrations",
1289
+ category: "advanced",
1290
+ url: `${BASE_URL}/advanced/custom-integrations`
1291
+ },
1292
+ { id: "value-exclusion", title: "Value Exclusion", category: "advanced", url: `${BASE_URL}/advanced/value-exclusion` },
1293
+ // AI Integration
1294
+ { id: "ai-integration", title: "AI Integration (MCP)", category: "ai", url: `${BASE_URL}/ai-integration` },
1295
+ // API Reference
1296
+ { id: "api-reference", title: "API Reference", category: "api", url: `${BASE_URL}/api` },
1297
+ // Examples
1298
+ { id: "examples", title: "Examples Overview", category: "examples", url: `${BASE_URL}/examples` },
1299
+ { id: "example-login", title: "Login Form", category: "examples", url: `${BASE_URL}/examples/login-form` },
1300
+ { id: "example-contact", title: "Contact Form", category: "examples", url: `${BASE_URL}/examples/contact-form` },
1301
+ { id: "example-registration", title: "User Registration", category: "examples", url: `${BASE_URL}/examples/user-registration` },
1302
+ { id: "example-paginated", title: "Multi-Page Wizard", category: "examples", url: `${BASE_URL}/examples/paginated-form` },
1303
+ { id: "example-derivation", title: "Value Derivation", category: "examples", url: `${BASE_URL}/examples/value-derivation` },
1304
+ {
1305
+ id: "example-dynamic-fields",
1306
+ title: "Dynamic Contact Fields",
1307
+ category: "examples",
1308
+ url: `${BASE_URL}/examples/contact-dynamic-fields`
1309
+ },
1310
+ { id: "example-business", title: "Business Account Form", category: "examples", url: `${BASE_URL}/examples/business-account-form` },
1311
+ {
1312
+ id: "example-shipping",
1313
+ title: "Shipping & Billing Address",
1314
+ category: "examples",
1315
+ url: `${BASE_URL}/examples/shipping-billing-address`
1316
+ },
1317
+ { id: "example-conditional", title: "Age Conditional Form", category: "examples", url: `${BASE_URL}/examples/age-conditional-form` },
1318
+ { id: "example-enterprise", title: "Enterprise Features", category: "examples", url: `${BASE_URL}/examples/enterprise-features` },
1319
+ {
1320
+ id: "example-simplified-array",
1321
+ title: "Simplified Array Form",
1322
+ category: "examples",
1323
+ url: `${BASE_URL}/examples/simplified-array-form`
1324
+ },
1325
+ { id: "example-array", title: "Complete Array Form", category: "examples", url: `${BASE_URL}/examples/array-form` }
1326
+ ];
1327
+
1226
1328
  // packages/dynamic-form-mcp/src/registry/index.ts
1227
- var fieldTypes = field_types_default;
1228
- var validators = validators_default;
1229
- var uiAdapters = ui_adapters_default;
1230
- var docsCache = null;
1231
1329
  function getFieldTypes() {
1232
- return fieldTypes;
1330
+ return FIELD_TYPES;
1233
1331
  }
1234
1332
  function getFieldType(type) {
1235
- return fieldTypes.find((ft) => ft.type === type);
1333
+ return FIELD_TYPES.find((ft) => ft.type === type);
1236
1334
  }
1237
1335
  function getFieldTypesByCategory(category) {
1238
- return fieldTypes.filter((ft) => ft.category === category);
1336
+ return FIELD_TYPES.filter((ft) => ft.category === category);
1239
1337
  }
1240
1338
  function getValidators() {
1241
- return validators;
1339
+ return VALIDATORS;
1242
1340
  }
1243
1341
  function getValidator(type) {
1244
- return validators.find((v) => v.type === type);
1342
+ return VALIDATORS.find((v) => v.type === type);
1245
1343
  }
1246
1344
  function getValidatorsByCategory(category) {
1247
- return validators.filter((v) => v.category === category);
1345
+ return VALIDATORS.filter((v) => v.category === category);
1248
1346
  }
1249
1347
  function getUIAdapters() {
1250
- return uiAdapters;
1348
+ return UI_ADAPTERS;
1251
1349
  }
1252
1350
  function getUIAdapter(library) {
1253
- return uiAdapters.find((a) => a.library === library);
1351
+ return UI_ADAPTERS.find((a) => a.library === library);
1254
1352
  }
1255
1353
  function getUIAdapterFieldType(library, fieldType) {
1256
1354
  const adapter = getUIAdapter(library);
1257
1355
  return adapter?.fieldTypes.find((ft) => ft.type === fieldType);
1258
1356
  }
1259
- function getDocs() {
1260
- if (!docsCache) {
1261
- try {
1262
- docsCache = require_docs();
1263
- } catch (error) {
1264
- if (process.env.NODE_ENV !== "production") {
1265
- const isModuleNotFound = error instanceof Error && "code" in error && error.code === "MODULE_NOT_FOUND";
1266
- if (!isModuleNotFound) {
1267
- console.warn("[ng-forge-mcp] Failed to load docs.json:", error instanceof Error ? error.message : error);
1268
- }
1269
- }
1270
- docsCache = [];
1271
- }
1272
- }
1273
- return docsCache;
1357
+ function getDocPages() {
1358
+ return DOCUMENTATION;
1274
1359
  }
1275
- function getDoc(id) {
1276
- return getDocs().find((d) => d.id === id);
1360
+ function getDocPage(id) {
1361
+ return DOCUMENTATION.find((d) => d.id === id);
1277
1362
  }
1278
- function getDocsByCategory(category) {
1279
- return getDocs().filter((d) => d.category === category);
1363
+ function getDocPagesByCategory(category) {
1364
+ return DOCUMENTATION.filter((d) => d.category === category);
1280
1365
  }
1281
1366
 
1282
1367
  // packages/dynamic-form-mcp/src/resources/field-types.resource.ts
1283
1368
  function registerFieldTypesResource(server) {
1284
1369
  server.resource("ng-forge Field Types", "ng-forge://field-types", async () => {
1285
- const fieldTypes2 = getFieldTypes();
1370
+ const fieldTypes = getFieldTypes();
1286
1371
  return {
1287
1372
  contents: [
1288
1373
  {
@@ -1297,7 +1382,7 @@ function registerFieldTypesResource(server) {
1297
1382
  button: "Action buttons for form interactions",
1298
1383
  display: "Display-only elements for static content"
1299
1384
  },
1300
- fieldTypes: fieldTypes2.map((ft) => ({
1385
+ fieldTypes: fieldTypes.map((ft) => ({
1301
1386
  type: ft.type,
1302
1387
  category: ft.category,
1303
1388
  description: ft.description,
@@ -1362,7 +1447,7 @@ function registerFieldTypesResource(server) {
1362
1447
  ]
1363
1448
  };
1364
1449
  }
1365
- const fieldTypes2 = getFieldTypesByCategory(category);
1450
+ const fieldTypes = getFieldTypesByCategory(category);
1366
1451
  return {
1367
1452
  contents: [
1368
1453
  {
@@ -1371,7 +1456,7 @@ function registerFieldTypesResource(server) {
1371
1456
  text: JSON.stringify(
1372
1457
  {
1373
1458
  category,
1374
- fieldTypes: fieldTypes2
1459
+ fieldTypes
1375
1460
  },
1376
1461
  null,
1377
1462
  2
@@ -1385,7 +1470,7 @@ function registerFieldTypesResource(server) {
1385
1470
  // packages/dynamic-form-mcp/src/resources/validators.resource.ts
1386
1471
  function registerValidatorsResource(server) {
1387
1472
  server.resource("ng-forge Validators", "ng-forge://validators", async () => {
1388
- const validators2 = getValidators();
1473
+ const validators = getValidators();
1389
1474
  return {
1390
1475
  contents: [
1391
1476
  {
@@ -1418,7 +1503,7 @@ function registerValidatorsResource(server) {
1418
1503
  async: "Asynchronous validators returning Observables",
1419
1504
  http: "HTTP-based validators with automatic request cancellation"
1420
1505
  },
1421
- validators: validators2.map((v) => ({
1506
+ validators: validators.map((v) => ({
1422
1507
  type: v.type,
1423
1508
  category: v.category,
1424
1509
  description: v.description
@@ -1481,7 +1566,7 @@ function registerValidatorsResource(server) {
1481
1566
  ]
1482
1567
  };
1483
1568
  }
1484
- const validators2 = getValidatorsByCategory(category);
1569
+ const validators = getValidatorsByCategory(category);
1485
1570
  return {
1486
1571
  contents: [
1487
1572
  {
@@ -1490,7 +1575,7 @@ function registerValidatorsResource(server) {
1490
1575
  text: JSON.stringify(
1491
1576
  {
1492
1577
  category,
1493
- validators: validators2
1578
+ validators
1494
1579
  },
1495
1580
  null,
1496
1581
  2
@@ -1596,10 +1681,92 @@ export const appConfig: ApplicationConfig = {
1596
1681
  });
1597
1682
  }
1598
1683
 
1684
+ // packages/dynamic-form-mcp/src/services/doc-fetcher.ts
1685
+ var BASE_URL2 = "https://ng-forge.com/dynamic-forms";
1686
+ var LLMS_TXT_URL = `${BASE_URL2}/llms.txt`;
1687
+ var LLMS_FULL_TXT_URL = `${BASE_URL2}/llms-full.txt`;
1688
+ var FETCH_TIMEOUT_MS = 5e3;
1689
+ var DEFAULT_TTL_MS = 10 * 60 * 1e3;
1690
+ var indexCache = null;
1691
+ var sectionsCache = null;
1692
+ var cacheTtlMs = DEFAULT_TTL_MS;
1693
+ function isFresh(entry) {
1694
+ return entry !== null && Date.now() - entry.timestamp < cacheTtlMs;
1695
+ }
1696
+ async function fetchWithTimeout(url) {
1697
+ try {
1698
+ const controller = new AbortController();
1699
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1700
+ const response = await fetch(url, { signal: controller.signal });
1701
+ clearTimeout(timeout);
1702
+ if (!response.ok) {
1703
+ return null;
1704
+ }
1705
+ return await response.text();
1706
+ } catch {
1707
+ return null;
1708
+ }
1709
+ }
1710
+ function parseSections(raw) {
1711
+ const sections = /* @__PURE__ */ new Map();
1712
+ const sectionRegex = /^--- (.+?) ---$/gm;
1713
+ const matches = [...raw.matchAll(sectionRegex)];
1714
+ for (let i = 0; i < matches.length; i++) {
1715
+ const match = matches[i];
1716
+ const path = match[1];
1717
+ const startIndex = (match.index ?? 0) + match[0].length;
1718
+ const nextMatch = matches[i + 1];
1719
+ const endIndex = nextMatch ? nextMatch.index ?? raw.length : raw.length;
1720
+ const content = raw.slice(startIndex, endIndex).trim();
1721
+ if (content) {
1722
+ sections.set(path, content);
1723
+ }
1724
+ }
1725
+ return sections;
1726
+ }
1727
+ async function fetchDocIndex() {
1728
+ if (isFresh(indexCache)) {
1729
+ return indexCache.data;
1730
+ }
1731
+ const text = await fetchWithTimeout(LLMS_TXT_URL);
1732
+ if (text) {
1733
+ indexCache = { data: text, timestamp: Date.now() };
1734
+ }
1735
+ return text;
1736
+ }
1737
+ async function fetchAllSections() {
1738
+ if (isFresh(sectionsCache)) {
1739
+ return sectionsCache.data;
1740
+ }
1741
+ const raw = await fetchWithTimeout(LLMS_FULL_TXT_URL);
1742
+ if (!raw) {
1743
+ return null;
1744
+ }
1745
+ const sections = parseSections(raw);
1746
+ sectionsCache = { data: sections, timestamp: Date.now() };
1747
+ return sections;
1748
+ }
1749
+ async function fetchDocSection(path) {
1750
+ const sections = await fetchAllSections();
1751
+ return sections?.get(path) ?? null;
1752
+ }
1753
+
1599
1754
  // packages/dynamic-form-mcp/src/resources/documentation.resource.ts
1600
1755
  function registerDocumentationResource(server) {
1601
1756
  server.resource("ng-forge Documentation", "ng-forge://docs", async () => {
1602
- const docs = getDocs();
1757
+ const liveIndex = await fetchDocIndex();
1758
+ if (liveIndex) {
1759
+ return {
1760
+ contents: [
1761
+ {
1762
+ uri: "ng-forge://docs",
1763
+ mimeType: "text/markdown",
1764
+ text: liveIndex
1765
+ }
1766
+ ]
1767
+ };
1768
+ }
1769
+ const docs = getDocPages();
1603
1770
  const categories = [...new Set(docs.map((d) => d.category))];
1604
1771
  return {
1605
1772
  contents: [
@@ -1608,17 +1775,18 @@ function registerDocumentationResource(server) {
1608
1775
  mimeType: "application/json",
1609
1776
  text: JSON.stringify(
1610
1777
  {
1611
- description: "ng-forge dynamic forms documentation",
1778
+ description: "ng-forge dynamic forms documentation \u2014 links to online docs at ng-forge.com",
1779
+ docsHome: "https://ng-forge.com/dynamic-forms/",
1780
+ llmsTxt: "https://ng-forge.com/dynamic-forms/llms.txt",
1612
1781
  totalTopics: docs.length,
1613
1782
  categories: categories.map((cat) => ({
1614
1783
  name: cat,
1615
1784
  topics: docs.filter((d) => d.category === cat).map((d) => ({
1616
1785
  id: d.id,
1617
1786
  title: d.title,
1618
- uri: `ng-forge://docs/${d.id}`
1787
+ url: d.url
1619
1788
  }))
1620
- })),
1621
- usage: "Access specific topics via ng-forge://docs/{topic-id}"
1789
+ }))
1622
1790
  },
1623
1791
  null,
1624
1792
  2
@@ -1631,40 +1799,56 @@ function registerDocumentationResource(server) {
1631
1799
  const match = uri.href.match(/ng-forge:\/\/docs\/(.+)/);
1632
1800
  const topicId = match?.[1];
1633
1801
  if (!topicId) {
1634
- const docs = getDocs();
1635
1802
  return {
1636
1803
  contents: [
1637
1804
  {
1638
1805
  uri: uri.href,
1639
1806
  mimeType: "text/plain",
1640
- text: "Topic not specified. Available topics: " + docs.map((d) => d.id).join(", ")
1807
+ text: "Topic not specified. Available topics: " + getDocPages().map((d) => d.id).join(", ")
1641
1808
  }
1642
1809
  ]
1643
1810
  };
1644
1811
  }
1645
- const categoryDocs = getDocsByCategory(topicId);
1646
- if (categoryDocs.length > 0 && !getDoc(topicId)) {
1812
+ const sections = await fetchAllSections();
1813
+ if (sections) {
1814
+ const liveContent = sections.get(topicId) ?? findSectionByPartialMatch(sections, topicId);
1815
+ if (liveContent) {
1816
+ return {
1817
+ contents: [
1818
+ {
1819
+ uri: uri.href,
1820
+ mimeType: "text/markdown",
1821
+ text: liveContent
1822
+ }
1823
+ ]
1824
+ };
1825
+ }
1826
+ }
1827
+ const categoryDocs = getDocPagesByCategory(topicId);
1828
+ if (categoryDocs.length > 0 && !getDocPage(topicId)) {
1829
+ const lines = categoryDocs.map((d) => `- [${d.title}](${d.url})`);
1647
1830
  return {
1648
1831
  contents: [
1649
1832
  {
1650
1833
  uri: uri.href,
1651
1834
  mimeType: "text/markdown",
1652
- text: categoryDocs.map((d) => `# ${d.title}
1835
+ text: `# ${topicId}
1653
1836
 
1654
- ${d.content}`).join("\n\n---\n\n")
1837
+ ${lines.join("\n")}`
1655
1838
  }
1656
1839
  ]
1657
1840
  };
1658
1841
  }
1659
- const doc = getDoc(topicId);
1660
- if (!doc) {
1661
- const docs = getDocs();
1842
+ const doc = getDocPage(topicId);
1843
+ if (doc) {
1662
1844
  return {
1663
1845
  contents: [
1664
1846
  {
1665
1847
  uri: uri.href,
1666
- mimeType: "text/plain",
1667
- text: `Topic '${topicId}' not found. Available topics: ${docs.map((d) => d.id).join(", ")}`
1848
+ mimeType: "text/markdown",
1849
+ text: `# ${doc.title}
1850
+
1851
+ Documentation: ${doc.url}`
1668
1852
  }
1669
1853
  ]
1670
1854
  };
@@ -1673,15 +1857,23 @@ ${d.content}`).join("\n\n---\n\n")
1673
1857
  contents: [
1674
1858
  {
1675
1859
  uri: uri.href,
1676
- mimeType: "text/markdown",
1677
- text: `# ${doc.title}
1678
-
1679
- ${doc.content}`
1860
+ mimeType: "text/plain",
1861
+ text: `Topic '${topicId}' not found. Available topics: ${getDocPages().map((d) => d.id).join(", ")}`
1680
1862
  }
1681
1863
  ]
1682
1864
  };
1683
1865
  });
1684
1866
  }
1867
+ function findSectionByPartialMatch(sections, query) {
1868
+ const matches = [];
1869
+ const normalizedQuery = query.toLowerCase();
1870
+ for (const [path, content] of sections) {
1871
+ if (path.toLowerCase().includes(normalizedQuery)) {
1872
+ matches.push(content);
1873
+ }
1874
+ }
1875
+ return matches.length > 0 ? matches.join("\n\n---\n\n") : null;
1876
+ }
1685
1877
 
1686
1878
  // packages/dynamic-form-mcp/src/registry/instructions.ts
1687
1879
  var INSTRUCTIONS = `# ng-forge Dynamic Forms - Best Practices
@@ -1918,12 +2110,103 @@ Use \`logic\` array for conditional behavior:
1918
2110
  - \`disabled\` - Enable/disable field
1919
2111
  - \`required\` - Conditional requirement
1920
2112
 
1921
- ### Condition Operators
2113
+ ### Condition Types
2114
+
2115
+ #### Field Value Condition
2116
+
2117
+ Compares a specific field's value:
2118
+
2119
+ \`\`\`typescript
2120
+ {
2121
+ type: 'fieldValue',
2122
+ fieldPath: 'accountType',
2123
+ operator: 'equals',
2124
+ value: 'business'
2125
+ }
2126
+ \`\`\`
2127
+
2128
+ #### Comparison Operators
1922
2129
 
1923
2130
  - \`equals\`, \`notEquals\`
1924
- - \`greaterThan\`, \`lessThan\`, \`greaterThanOrEquals\`, \`lessThanOrEquals\`
1925
- - \`contains\`, \`notContains\`
1926
- - \`empty\`, \`notEmpty\`
2131
+ - \`greater\`, \`less\`, \`greaterOrEqual\`, \`lessOrEqual\`
2132
+ - \`contains\`, \`startsWith\`, \`endsWith\`, \`matches\`
2133
+
2134
+ #### JavaScript Condition
2135
+
2136
+ Evaluates a JavaScript expression with access to \`formValue\`, \`fieldValue\`, \`externalData\`:
2137
+
2138
+ \`\`\`typescript
2139
+ {
2140
+ type: 'javascript',
2141
+ expression: 'formValue.items.length > 0'
2142
+ }
2143
+ \`\`\`
2144
+
2145
+ #### Custom Condition
2146
+
2147
+ Invokes a registered custom function by name:
2148
+
2149
+ \`\`\`typescript
2150
+ {
2151
+ type: 'custom',
2152
+ functionName: 'myCustomCheck'
2153
+ }
2154
+ \`\`\`
2155
+
2156
+ Register functions via \`customFnConfig.customFunctions\`.
2157
+
2158
+ #### Async Custom Condition
2159
+
2160
+ Invokes a registered async function that returns \`Promise<boolean>\` or \`Observable<boolean>\`:
2161
+
2162
+ \`\`\`typescript
2163
+ {
2164
+ type: 'async',
2165
+ asyncFunctionName: 'checkPermission',
2166
+ pendingValue: false, // value while async resolution is pending (default: false)
2167
+ debounceMs: 300 // debounce for re-evaluation (default: 300)
2168
+ }
2169
+ \`\`\`
2170
+
2171
+ Register functions via \`customFnConfig.asyncConditions\`. The function receives an \`EvaluationContext\` with access to \`formValue\`, \`fieldValue\`, and \`externalData\`.
2172
+
2173
+ #### HTTP Condition
2174
+
2175
+ Evaluates based on an HTTP response from a remote server. Fully declarative and JSON-serializable:
2176
+
2177
+ \`\`\`typescript
2178
+ {
2179
+ type: 'http',
2180
+ http: {
2181
+ url: '/api/permissions',
2182
+ method: 'GET',
2183
+ queryParams: {
2184
+ userId: 'formValue.userId',
2185
+ role: 'formValue.role'
2186
+ }
2187
+ },
2188
+ responseExpression: 'response.canEdit', // extract boolean from response (default: !!response)
2189
+ pendingValue: false, // value while HTTP request is in-flight (default: false)
2190
+ cacheDurationMs: 30000, // cache duration in ms (default: 30000)
2191
+ debounceMs: 300 // debounce for re-evaluation (default: 300)
2192
+ }
2193
+ \`\`\`
2194
+
2195
+ Use HTTP conditions when visibility, disabled state, or required state depends on server-side data (e.g., permissions, feature flags, inventory checks). The request re-evaluates reactively when dependent form values change.
2196
+
2197
+ #### Logical Combinators
2198
+
2199
+ Combine conditions with \`and\` (all must be true) or \`or\` (at least one must be true):
2200
+
2201
+ \`\`\`typescript
2202
+ {
2203
+ type: 'and',
2204
+ conditions: [
2205
+ { type: 'fieldValue', fieldPath: 'country', operator: 'equals', value: 'US' },
2206
+ { type: 'fieldValue', fieldPath: 'age', operator: 'greaterOrEqual', value: 18 }
2207
+ ]
2208
+ }
2209
+ \`\`\`
1927
2210
 
1928
2211
  ## UI Library Integration
1929
2212
 
@@ -5405,9 +5688,9 @@ function preValidateConfig(config) {
5405
5688
  const f = field;
5406
5689
  const fieldType = f["type"];
5407
5690
  const fieldKey = f["key"];
5408
- const validators2 = f["validators"];
5409
- if (Array.isArray(validators2)) {
5410
- validators2.forEach((validator, vIdx) => {
5691
+ const validators = f["validators"];
5692
+ if (Array.isArray(validators)) {
5693
+ validators.forEach((validator, vIdx) => {
5411
5694
  if (validator && typeof validator === "object") {
5412
5695
  const v = validator;
5413
5696
  for (const prop of INVALID_VALIDATOR_PROPS) {
@@ -5645,7 +5928,7 @@ function isValidFormConfig(uiIntegration, config) {
5645
5928
  var UI_INTEGRATIONS = ["material", "bootstrap", "primeng", "ionic"];
5646
5929
  function registerSchemasResource(server) {
5647
5930
  server.resource("ng-forge Schemas", "ng-forge://schemas", async () => {
5648
- const fieldTypes2 = getSupportedFieldTypes();
5931
+ const fieldTypes = getSupportedFieldTypes();
5649
5932
  return {
5650
5933
  contents: [
5651
5934
  {
@@ -5657,7 +5940,7 @@ function registerSchemasResource(server) {
5657
5940
  ${UI_INTEGRATIONS.map((ui) => `- ${ui}`).join("\n")}
5658
5941
 
5659
5942
  ## Field Types
5660
- ${fieldTypes2.map((ft) => `- ${ft}`).join("\n")}
5943
+ ${fieldTypes.map((ft) => `- ${ft}`).join("\n")}
5661
5944
 
5662
5945
  ## Tools (4 total - consolidated)
5663
5946
 
@@ -8066,6 +8349,50 @@ Catches errors with specific fix suggestions.
8066
8349
  4. **Options at field level, appearance in props** - This is the #1 gotcha`
8067
8350
  }
8068
8351
  };
8352
+ var TOPIC_DESCRIPTIONS = {
8353
+ // Field types
8354
+ input: "Text input field (text, email, password, number, tel, url)",
8355
+ select: "Dropdown select with options at field level",
8356
+ slider: "Numeric range slider (minValue/maxValue/step at field level)",
8357
+ radio: "Radio button group with options at field level",
8358
+ checkbox: "Single boolean checkbox or multi-checkbox for multiple selections",
8359
+ textarea: "Multi-line text area with rows and maxLength",
8360
+ datepicker: "Date picker with startView, min/max date support",
8361
+ toggle: "Boolean on/off toggle switch",
8362
+ text: "Display-only text/heading (content in label, element in props.elementType)",
8363
+ hidden: "Hidden field included in form value (value is REQUIRED, no UI)",
8364
+ // Containers
8365
+ group: "Nested object container \u2014 creates { key: { ...children } }",
8366
+ row: "Horizontal layout container with col-based grid (1-12)",
8367
+ array: "Dynamic list with add/remove \u2014 simplified (template) or full (fields) API",
8368
+ "simplified-array": "Simplified array API using template + value for common patterns",
8369
+ page: "Multi-page wizard step with next/previous/submit navigation",
8370
+ // Concepts
8371
+ validation: "Built-in validators and shorthand properties (required, email, min, max, pattern)",
8372
+ "validation-messages": "Custom error messages for validators via validationMessages",
8373
+ conditional: "Show/hide/disable fields based on conditions (logic array)",
8374
+ derivation: "Computed field values from expressions or functions",
8375
+ "property-derivation": "Derive field properties (options, label, minDate) from form values",
8376
+ "options-format": "Option format for select/radio/multi-checkbox ({ label, value } at field level)",
8377
+ "expression-variables": "Variables available in expressions: formValue, fieldValue, externalData",
8378
+ "async-validators": "HTTP-based async validators with responseMapping and validWhen",
8379
+ buttons: "Button types: submit, next, previous, button, add-array-item, remove-array-item",
8380
+ "external-data": "Injecting application state (roles, permissions, feature flags) into forms",
8381
+ // Patterns & Rules
8382
+ "golden-path": "Recommended form structures and best practices",
8383
+ "multi-page-gotchas": "Common pitfalls with multi-page wizard forms",
8384
+ pitfalls: "Top mistakes: options in props, label on containers, hidden without value",
8385
+ "field-placement": "Which fields can go where \u2014 nesting and placement rules",
8386
+ "logic-matrix": "Which logic types are supported on which field types",
8387
+ "context-api": "FieldContext API \u2014 valueOf(), formValue, parent, arrayIndex",
8388
+ containers: "Overview of all container types (group, row, array, page)",
8389
+ "array-buttons": "Add/remove array item button configuration",
8390
+ "custom-validators": "Custom validator functions via validators array",
8391
+ conditions: "Condition operators and types for logic rules",
8392
+ "common-expressions": "Frequently used derivation expressions (age, math, concatenation)",
8393
+ "type-narrowing": "TypeScript type safety with as const satisfies FormConfig",
8394
+ workflow: "Recommended tool usage guide \u2014 start here"
8395
+ };
8069
8396
  var TOPIC_ALIASES = {
8070
8397
  // Field type aliases
8071
8398
  textfield: "input",
@@ -8194,6 +8521,76 @@ var TOPIC_ALIASES = {
8194
8521
  "getting-started": "workflow"
8195
8522
  };
8196
8523
 
8524
+ // packages/dynamic-form-mcp/src/services/topic-mapper.ts
8525
+ var TOPIC_SECTION_MAP = {
8526
+ // Field types
8527
+ input: ["schema-fields/field-types"],
8528
+ select: ["schema-fields/field-types"],
8529
+ slider: ["schema-fields/field-types"],
8530
+ radio: ["schema-fields/field-types"],
8531
+ checkbox: ["schema-fields/field-types"],
8532
+ textarea: ["schema-fields/field-types"],
8533
+ datepicker: ["schema-fields/field-types"],
8534
+ toggle: ["schema-fields/field-types"],
8535
+ text: ["prebuilt/text-components"],
8536
+ hidden: ["prebuilt/hidden-fields"],
8537
+ // Containers
8538
+ group: ["prebuilt/form-groups"],
8539
+ row: ["prebuilt/form-rows"],
8540
+ array: ["prebuilt/form-arrays/simplified", "prebuilt/form-arrays/complete"],
8541
+ "simplified-array": ["prebuilt/form-arrays/simplified"],
8542
+ page: ["prebuilt/form-pages"],
8543
+ // Concepts
8544
+ validation: ["validation/basics"],
8545
+ "validation-messages": ["validation/basics"],
8546
+ conditional: ["dynamic-behavior/conditional-logic/overview"],
8547
+ derivation: ["dynamic-behavior/value-derivation/basics"],
8548
+ "property-derivation": ["dynamic-behavior/value-derivation/basics/property-derivation"],
8549
+ "async-validators": ["dynamic-behavior/value-derivation/basics/async-derivation"],
8550
+ "options-format": ["schema-fields/field-types"],
8551
+ "expression-variables": ["advanced/expression-parser"],
8552
+ buttons: ["prebuilt/form-pages"],
8553
+ // Patterns with website equivalents
8554
+ containers: ["prebuilt/form-groups", "prebuilt/form-rows", "prebuilt/form-pages"],
8555
+ "custom-validators": ["validation/custom-validators"],
8556
+ conditions: ["dynamic-behavior/conditional-logic/overview"],
8557
+ "common-expressions": ["advanced/expression-parser"],
8558
+ "type-narrowing": ["advanced/type-safety/basics"],
8559
+ // Advanced
8560
+ "external-data": ["advanced/expression-parser"],
8561
+ i18n: ["dynamic-behavior/i18n"],
8562
+ submission: ["dynamic-behavior/submission"],
8563
+ // Schema validation
8564
+ "schema-overview": ["schema-validation/overview"],
8565
+ "schema-angular": ["schema-validation/angular-schema"],
8566
+ "schema-zod": ["schema-validation/zod"],
8567
+ // Installation
8568
+ installation: ["installation"],
8569
+ // UI integrations
8570
+ "ui-material": ["ui-libs-integrations/material"],
8571
+ "ui-primeng": ["ui-libs-integrations/primeng"],
8572
+ "ui-ionic": ["ui-libs-integrations/ionic"],
8573
+ "ui-bootstrap": ["ui-libs-integrations/bootstrap"],
8574
+ // AI integration
8575
+ "ai-integration": ["ai-integration"]
8576
+ };
8577
+ var MCP_ONLY_TOPICS = /* @__PURE__ */ new Set([
8578
+ "golden-path",
8579
+ "pitfalls",
8580
+ "field-placement",
8581
+ "logic-matrix",
8582
+ "context-api",
8583
+ "multi-page-gotchas",
8584
+ "array-buttons",
8585
+ "workflow"
8586
+ ]);
8587
+ function getTopicSections(topic) {
8588
+ if (MCP_ONLY_TOPICS.has(topic)) {
8589
+ return null;
8590
+ }
8591
+ return TOPIC_SECTION_MAP[topic];
8592
+ }
8593
+
8197
8594
  // packages/dynamic-form-mcp/src/tools/lookup.tool.ts
8198
8595
  var UI_INTEGRATIONS2 = ["material", "bootstrap", "primeng", "ionic"];
8199
8596
  var SCHEMA_SUPPORTED_FIELD_TYPES = [
@@ -8211,9 +8608,13 @@ var SCHEMA_SUPPORTED_FIELD_TYPES = [
8211
8608
  "next",
8212
8609
  "previous"
8213
8610
  ];
8611
+ function formatTopicItem(key) {
8612
+ const desc = TOPIC_DESCRIPTIONS[key];
8613
+ return desc ? `- **${key}**: ${desc}` : `- **${key}**`;
8614
+ }
8214
8615
  function getTopicList() {
8215
- const fieldTypes2 = ["input", "select", "slider", "radio", "checkbox", "textarea", "datepicker", "toggle", "text", "hidden"];
8216
- const containers = ["group", "row", "array", "page"];
8616
+ const fieldTypes = ["input", "select", "slider", "radio", "checkbox", "textarea", "datepicker", "toggle", "text", "hidden"];
8617
+ const containers = ["group", "row", "array", "simplified-array", "page"];
8217
8618
  const concepts = [
8218
8619
  "validation",
8219
8620
  "validation-messages",
@@ -8223,7 +8624,8 @@ function getTopicList() {
8223
8624
  "options-format",
8224
8625
  "expression-variables",
8225
8626
  "async-validators",
8226
- "buttons"
8627
+ "buttons",
8628
+ "external-data"
8227
8629
  ];
8228
8630
  const patterns = [
8229
8631
  "golden-path",
@@ -8243,18 +8645,20 @@ function getTopicList() {
8243
8645
  return `# Available Topics
8244
8646
 
8245
8647
  ## Field Types
8246
- ${fieldTypes2.join(", ")}
8648
+ ${fieldTypes.map(formatTopicItem).join("\n")}
8247
8649
 
8248
8650
  ## Containers
8249
- ${containers.join(", ")}
8651
+ ${containers.map(formatTopicItem).join("\n")}
8250
8652
 
8251
8653
  ## Concepts
8252
- ${concepts.join(", ")}
8654
+ ${concepts.map(formatTopicItem).join("\n")}
8253
8655
 
8254
8656
  ## Patterns & Rules
8255
- ${patterns.join(", ")}
8657
+ ${patterns.map(formatTopicItem).join("\n")}
8256
8658
 
8257
- **Usage:** \`ngforge_lookup topic="<topic>" depth="brief|full|schema"\``;
8659
+ **Usage:** \`ngforge_lookup topic="<topic>" depth="brief|full|schema"\`
8660
+
8661
+ **Search:** \`ngforge_search query="your question"\` \u2014 find topics by keyword`;
8258
8662
  }
8259
8663
  function formatFieldInfoFull(field, uiFieldType) {
8260
8664
  const lines = [];
@@ -8312,25 +8716,121 @@ function formatFieldInfoFull(field, uiFieldType) {
8312
8716
  lines.push("```");
8313
8717
  return lines.join("\n");
8314
8718
  }
8719
+ async function resolveTopicContent(resolvedTopic, depth, uiIntegration) {
8720
+ if (depth === "brief") {
8721
+ const topicContent2 = TOPICS[resolvedTopic];
8722
+ if (topicContent2) {
8723
+ return topicContent2.brief;
8724
+ }
8725
+ }
8726
+ if (depth !== "brief") {
8727
+ const sectionPaths = getTopicSections(resolvedTopic);
8728
+ if (sectionPaths && sectionPaths.length > 0) {
8729
+ const liveSections = [];
8730
+ for (const path of sectionPaths) {
8731
+ const section = await fetchDocSection(path);
8732
+ if (section) {
8733
+ liveSections.push(section);
8734
+ }
8735
+ }
8736
+ if (liveSections.length > 0) {
8737
+ let content = liveSections.join("\n\n---\n\n");
8738
+ content = appendJsonSchema(content, resolvedTopic, depth, uiIntegration);
8739
+ return content;
8740
+ }
8741
+ }
8742
+ }
8743
+ const topicContent = TOPICS[resolvedTopic];
8744
+ if (topicContent) {
8745
+ let content = depth === "brief" ? topicContent.brief : topicContent.full;
8746
+ content = appendJsonSchema(content, resolvedTopic, depth, uiIntegration);
8747
+ return content;
8748
+ }
8749
+ const fieldInfo = getFieldType(resolvedTopic);
8750
+ if (fieldInfo) {
8751
+ let uiFieldType;
8752
+ if (uiIntegration) {
8753
+ const adapter = getUIAdapter(uiIntegration);
8754
+ uiFieldType = adapter?.fieldTypes.find((ft) => ft.type === resolvedTopic);
8755
+ }
8756
+ let content = formatFieldInfoFull(fieldInfo, uiFieldType);
8757
+ content = appendJsonSchema(content, resolvedTopic, depth, uiIntegration);
8758
+ return content;
8759
+ }
8760
+ return null;
8761
+ }
8762
+ function appendJsonSchema(content, resolvedTopic, depth, uiIntegration) {
8763
+ if (depth === "schema" && uiIntegration) {
8764
+ const fieldType = resolvedTopic;
8765
+ if (SCHEMA_SUPPORTED_FIELD_TYPES.includes(fieldType)) {
8766
+ const schema = getFieldTypeJsonSchema(uiIntegration, fieldType);
8767
+ content += `
8768
+
8769
+ ### JSON Schema
8770
+
8771
+ \`\`\`json
8772
+ ${JSON.stringify(schema, null, 2)}
8773
+ \`\`\``;
8774
+ }
8775
+ }
8776
+ return content;
8777
+ }
8778
+ var NEXT_STEPS = {
8779
+ // Field types → validate + related topics
8780
+ input: '**Next:** `ngforge_validate` to check your config \xB7 `ngforge_lookup topic="validation"` for validators \xB7 `ngforge_lookup topic="options-format"` if using select/radio',
8781
+ select: '**Next:** `ngforge_validate` to check your config \xB7 `ngforge_lookup topic="options-format"` for option syntax \xB7 `ngforge_lookup topic="property-derivation"` for dynamic options',
8782
+ slider: '**Next:** `ngforge_validate` to check your config \xB7 `ngforge_lookup topic="derivation"` for computed values',
8783
+ radio: '**Next:** `ngforge_validate` to check your config \xB7 `ngforge_lookup topic="options-format"` for option syntax',
8784
+ checkbox: '**Next:** `ngforge_validate` to check your config \xB7 `ngforge_lookup topic="validation"` for required checkbox',
8785
+ datepicker: '**Next:** `ngforge_validate` to check your config \xB7 `ngforge_lookup topic="property-derivation"` for dynamic min/max dates',
8786
+ hidden: '**Next:** `ngforge_lookup topic="multi-page-gotchas"` for hidden field placement \xB7 `ngforge_examples pattern="minimal-hidden"` for working example',
8787
+ // Containers
8788
+ group: '**Next:** `ngforge_lookup topic="field-placement"` for nesting rules \xB7 `ngforge_lookup topic="containers"` for all container types',
8789
+ row: '**Next:** `ngforge_lookup topic="field-placement"` for nesting rules \xB7 `ngforge_lookup topic="containers"` for all container types',
8790
+ array: '**Next:** `ngforge_lookup topic="simplified-array"` for the simpler API \xB7 `ngforge_lookup topic="array-buttons"` for button config \xB7 `ngforge_examples pattern="minimal-array"` for working example',
8791
+ "simplified-array": '**Next:** `ngforge_examples pattern="minimal-simplified-array"` for working example \xB7 `ngforge_lookup topic="array-buttons"` for button config',
8792
+ page: '**Next:** `ngforge_lookup topic="multi-page-gotchas"` for common pitfalls \xB7 `ngforge_examples pattern="minimal-multipage"` for working example',
8793
+ // Concepts
8794
+ validation: '**Next:** `ngforge_lookup topic="custom-validators"` for custom functions \xB7 `ngforge_lookup topic="validation-messages"` for error messages \xB7 `ngforge_lookup topic="async-validators"` for HTTP validators',
8795
+ "validation-messages": '**Next:** `ngforge_lookup topic="validation"` for validator types \xB7 `ngforge_lookup topic="custom-validators"` for custom validators',
8796
+ conditional: '**Next:** `ngforge_lookup topic="conditions"` for operator reference \xB7 `ngforge_lookup topic="logic-matrix"` for supported logic types \xB7 `ngforge_examples pattern="minimal-conditional"` for working example',
8797
+ derivation: '**Next:** `ngforge_lookup topic="expression-variables"` for available variables \xB7 `ngforge_lookup topic="common-expressions"` for expression recipes \xB7 `ngforge_examples pattern="derivation"` for working example',
8798
+ "property-derivation": '**Next:** `ngforge_lookup topic="derivation"` for value derivations \xB7 `ngforge_examples pattern="property-derivation"` for working example',
8799
+ "options-format": '**Next:** `ngforge_lookup topic="select"` or `ngforge_lookup topic="radio"` for field types that use options',
8800
+ "async-validators": '**Next:** `ngforge_lookup topic="validation"` for sync validators \xB7 `ngforge_lookup topic="custom-validators"` for custom functions',
8801
+ // Patterns
8802
+ "golden-path": '**Next:** `ngforge_examples pattern="complete"` for full working example \xB7 `ngforge_lookup topic="pitfalls"` for what to avoid',
8803
+ pitfalls: '**Next:** `ngforge_lookup topic="golden-path"` for recommended structures \xB7 `ngforge_validate` to check your config',
8804
+ "logic-matrix": '**Next:** `ngforge_lookup topic="conditional"` for condition syntax \xB7 `ngforge_lookup topic="derivation"` for derivation syntax',
8805
+ "field-placement": '**Next:** `ngforge_lookup topic="containers"` for container overview',
8806
+ conditions: '**Next:** `ngforge_lookup topic="conditional"` for full conditional logic docs \xB7 `ngforge_examples pattern="minimal-conditional"` for working example',
8807
+ workflow: '**Next:** `ngforge_lookup topic="golden-path"` for form structure templates \xB7 `ngforge_examples pattern="complete"` for a full working example'
8808
+ };
8809
+ function getNextSteps(topic) {
8810
+ return NEXT_STEPS[topic];
8811
+ }
8315
8812
  function registerLookupTool(server) {
8316
8813
  server.tool(
8317
8814
  "ngforge_lookup",
8318
8815
  `DOCUMENTATION: Look up ng-forge Dynamic Forms topics - "Tell me about X"
8319
8816
 
8817
+ \u2B50 START HERE: ngforge_lookup topic="workflow" \u2014 tool usage guide and recommended flow
8818
+
8320
8819
  Recommended starting topics:
8321
- - workflow: Tool usage guide
8820
+ - workflow: Tool usage guide (START HERE for new sessions)
8322
8821
  - golden-path: Recommended form structures
8323
8822
  - pitfalls: Common mistakes and solutions
8324
8823
 
8325
8824
  Field types: input, select, slider, radio, checkbox, textarea, datepicker, toggle, text, hidden
8326
8825
 
8327
- Containers: group, row, array, page
8826
+ Containers: group, row, array, simplified-array, page
8328
8827
 
8329
8828
  Concepts: validation, conditional, derivation, property-derivation, options-format, expression-variables, async-validators
8330
8829
 
8331
8830
  Patterns: field-placement, logic-matrix, context-api, containers, multi-page-gotchas
8332
8831
 
8333
- Use topic="list" to see all available topics.`,
8832
+ Use topic="list" to see all available topics with descriptions.
8833
+ Use ngforge_search query="your question" to find topics by keyword.`,
8334
8834
  {
8335
8835
  topic: z54.string().describe(
8336
8836
  'Topic to look up: field types (input, select, hidden, group, row, array, page), concepts (validation, conditional, derivation, property-derivation, options-format), patterns (golden-path, pitfalls, multi-page-gotchas), or "list" to see all topics'
@@ -8346,45 +8846,16 @@ Use topic="list" to see all available topics.`,
8346
8846
  };
8347
8847
  }
8348
8848
  const resolvedTopic = TOPIC_ALIASES[normalizedTopic] || normalizedTopic;
8349
- const topicContent = TOPICS[resolvedTopic];
8350
- if (topicContent) {
8351
- let content = depth === "brief" ? topicContent.brief : topicContent.full;
8352
- if (depth === "schema" && uiIntegration) {
8353
- const fieldType = resolvedTopic;
8354
- if (SCHEMA_SUPPORTED_FIELD_TYPES.includes(fieldType)) {
8355
- const schema = getFieldTypeJsonSchema(uiIntegration, fieldType);
8356
- content += `
8357
-
8358
- ### JSON Schema
8359
-
8360
- \`\`\`json
8361
- ${JSON.stringify(schema, null, 2)}
8362
- \`\`\``;
8363
- }
8364
- }
8365
- return {
8366
- content: [{ type: "text", text: content }]
8367
- };
8368
- }
8369
- const fieldInfo = getFieldType(resolvedTopic);
8370
- if (fieldInfo) {
8371
- let uiFieldType;
8372
- if (uiIntegration) {
8373
- const adapter = getUIAdapter(uiIntegration);
8374
- uiFieldType = adapter?.fieldTypes.find((ft) => ft.type === resolvedTopic);
8375
- }
8376
- let content = formatFieldInfoFull(fieldInfo, uiFieldType);
8377
- if (depth === "schema" && uiIntegration) {
8378
- const fieldType = resolvedTopic;
8379
- if (SCHEMA_SUPPORTED_FIELD_TYPES.includes(fieldType)) {
8380
- const schema = getFieldTypeJsonSchema(uiIntegration, fieldType);
8849
+ let content = await resolveTopicContent(resolvedTopic, depth, uiIntegration);
8850
+ if (content) {
8851
+ if (depth !== "brief") {
8852
+ const nextSteps = getNextSteps(resolvedTopic);
8853
+ if (nextSteps) {
8381
8854
  content += `
8382
8855
 
8383
- ### JSON Schema
8856
+ ---
8384
8857
 
8385
- \`\`\`json
8386
- ${JSON.stringify(schema, null, 2)}
8387
- \`\`\``;
8858
+ ${nextSteps}`;
8388
8859
  }
8389
8860
  }
8390
8861
  return {
@@ -8392,7 +8863,7 @@ ${JSON.stringify(schema, null, 2)}
8392
8863
  };
8393
8864
  }
8394
8865
  const availableTopics = Object.keys(TOPICS).sort().join(", ");
8395
- const fieldTypes2 = getFieldTypes().map((t) => t.type).join(", ");
8866
+ const fieldTypes = getFieldTypes().map((t) => t.type).join(", ");
8396
8867
  return {
8397
8868
  content: [
8398
8869
  {
@@ -8401,7 +8872,7 @@ ${JSON.stringify(schema, null, 2)}
8401
8872
 
8402
8873
  **Available topics:** ${availableTopics}
8403
8874
 
8404
- **Field types:** ${fieldTypes2}
8875
+ **Field types:** ${fieldTypes}
8405
8876
 
8406
8877
  **Tip:** Try "list" to see all topics grouped by category.`
8407
8878
  }
@@ -10692,28 +11163,6 @@ Use depth="minimal" for code-only output (no markdown).`,
10692
11163
  content: [{ type: "text", text: content }]
10693
11164
  };
10694
11165
  }
10695
- const docPatterns = {
10696
- "dynamic-options": ["contact-dynamic-fields"],
10697
- "nested-groups": ["shipping-billing-address"],
10698
- i18n: ["dynamic-behavior-i18n"],
10699
- submission: ["dynamic-behavior-submission"]
10700
- };
10701
- const docIds = docPatterns[normalizedPattern];
10702
- if (docIds) {
10703
- const docs = [];
10704
- for (const id of docIds) {
10705
- const doc = getDoc(id);
10706
- if (doc) docs.push(doc);
10707
- }
10708
- if (docs.length > 0) {
10709
- const content = docs.map((d) => `# ${d.title}
10710
-
10711
- ${d.content}`).join("\n\n---\n\n");
10712
- return {
10713
- content: [{ type: "text", text: content }]
10714
- };
10715
- }
10716
- }
10717
11166
  return {
10718
11167
  content: [
10719
11168
  {
@@ -11149,8 +11598,39 @@ var FIX_SUGGESTIONS = {
11149
11598
  hidden: "Hidden fields do NOT support `hidden`. Remove it (the field is already hidden).",
11150
11599
  col: "Hidden fields do NOT support `col`. Remove it (no layout needed).",
11151
11600
  props: "Hidden fields do NOT support `props`. Remove it.",
11152
- arrayKey: "arrayKey should be at FIELD level, not inside props."
11601
+ arrayKey: "arrayKey should be at FIELD level, not inside props.",
11602
+ responseMapping: 'Declarative HTTP validators require `responseMapping: { validWhen: "response.isValid", errorKind: "serverError" }`. `validWhen` is an expression evaluated with `{ response }` scope \u2014 truthy means valid. `errorKind` maps to `validationMessages`.',
11603
+ validWhen: '`validWhen` is a property of `responseMapping`, not a top-level validator property. Use: `responseMapping: { validWhen: "response.ok", errorKind: "..." }`.',
11604
+ errorKind: '`errorKind` is a property of `responseMapping`, not a top-level validator property. Use: `responseMapping: { validWhen: "...", errorKind: "myError" }`.',
11605
+ stopOnUserOverride: "`stopOnUserOverride` is a derivation option. When true, the derivation stops running after the user manually edits the field. Pair with `reEngageOnDependencyChange: true` to re-derive when dependencies change.",
11606
+ reEngageOnDependencyChange: "`reEngageOnDependencyChange` requires `stopOnUserOverride: true`. Clears the user-override flag when dependencies change, allowing the derivation to run again.",
11607
+ httpCondition: 'HTTP conditions use `type: "http"` with `http: { url, method?, queryParams? }` and optional `responseExpression` to extract boolean from response. Set `pendingValue` for in-flight behavior.'
11153
11608
  };
11609
+ var ERROR_TOPIC_HINTS = [
11610
+ { pattern: /options/i, hint: '`ngforge_lookup topic="options-format"` \u2014 correct options syntax' },
11611
+ { pattern: /hidden/i, hint: '`ngforge_lookup topic="hidden"` \u2014 hidden field rules' },
11612
+ { pattern: /container|group|row|page/i, hint: '`ngforge_lookup topic="containers"` \u2014 container field rules' },
11613
+ { pattern: /logic|conditional/i, hint: '`ngforge_lookup topic="conditional"` \u2014 logic/conditional syntax' },
11614
+ { pattern: /validator|required|pattern|email/i, hint: '`ngforge_lookup topic="validation"` \u2014 validation rules' },
11615
+ { pattern: /derivation|expression/i, hint: '`ngforge_lookup topic="derivation"` \u2014 derivation syntax' },
11616
+ { pattern: /template|simplified/i, hint: '`ngforge_lookup topic="simplified-array"` \u2014 simplified array API' },
11617
+ { pattern: /responseMapping|validWhen/i, hint: '`ngforge_lookup topic="async-validators"` \u2014 async validator config' },
11618
+ { pattern: /array/i, hint: '`ngforge_lookup topic="array"` \u2014 array field configuration' }
11619
+ ];
11620
+ function collectErrorTopicHints(errors) {
11621
+ const seenHints = /* @__PURE__ */ new Set();
11622
+ const hints = [];
11623
+ for (const error of errors) {
11624
+ const textToSearch = `${error.path} ${error.message}`;
11625
+ for (const { pattern, hint } of ERROR_TOPIC_HINTS) {
11626
+ if (pattern.test(textToSearch) && !seenHints.has(hint)) {
11627
+ seenHints.add(hint);
11628
+ hints.push(hint);
11629
+ }
11630
+ }
11631
+ }
11632
+ return hints;
11633
+ }
11154
11634
  function getFixSuggestion(error) {
11155
11635
  const pathParts = error.path.split(".");
11156
11636
  const lastPart = pathParts[pathParts.length - 1];
@@ -11211,6 +11691,7 @@ function formatFileReport(filePath, uiIntegration, candidates, results) {
11211
11691
  } else {
11212
11692
  lines.push(`### \u274C ${totalErrors} Error(s) Found`);
11213
11693
  lines.push("");
11694
+ const allErrors = [];
11214
11695
  for (const result of results) {
11215
11696
  if (result.validation.valid) {
11216
11697
  lines.push(`#### ${result.name} (line ${result.line}): \u2705 Valid`);
@@ -11225,10 +11706,19 @@ function formatFileReport(filePath, uiIntegration, candidates, results) {
11225
11706
  lines.push(` - **Fix:** ${fix}`);
11226
11707
  }
11227
11708
  }
11709
+ allErrors.push(...result.validation.errors);
11228
11710
  }
11229
11711
  }
11230
11712
  lines.push("");
11231
11713
  }
11714
+ const hints = collectErrorTopicHints(allErrors);
11715
+ if (hints.length > 0) {
11716
+ lines.push("### \u{1F4DA} Related Documentation");
11717
+ for (const hint of hints) {
11718
+ lines.push(`- ${hint}`);
11719
+ }
11720
+ lines.push("");
11721
+ }
11232
11722
  }
11233
11723
  return lines.join("\n");
11234
11724
  }
@@ -11254,9 +11744,17 @@ function formatJsonReport(uiIntegration, result) {
11254
11744
  lines.push(` - **Fix:** ${fix}`);
11255
11745
  }
11256
11746
  }
11747
+ lines.push("");
11748
+ const hints = collectErrorTopicHints(result.errors);
11749
+ if (hints.length > 0) {
11750
+ lines.push("### \u{1F4DA} Related Documentation");
11751
+ for (const hint of hints) {
11752
+ lines.push(`- ${hint}`);
11753
+ }
11754
+ }
11755
+ lines.push("");
11756
+ lines.push('**Tip:** Use `ngforge_lookup topic="pitfalls"` for common mistakes and solutions.');
11257
11757
  }
11258
- lines.push("");
11259
- lines.push('**Tip:** Use `ngforge_lookup topic="pitfalls"` for common mistakes and solutions.');
11260
11758
  }
11261
11759
  return lines.join("\n");
11262
11760
  }
@@ -11716,12 +12214,161 @@ Examples:
11716
12214
  );
11717
12215
  }
11718
12216
 
12217
+ // packages/dynamic-form-mcp/src/tools/search.tool.ts
12218
+ import { z as z58 } from "zod";
12219
+ var searchIndex = null;
12220
+ function getTopicSubcategory(key) {
12221
+ const fieldTypes = ["input", "select", "slider", "radio", "checkbox", "textarea", "datepicker", "toggle", "text", "hidden"];
12222
+ const containers = ["group", "row", "array", "simplified-array", "page"];
12223
+ const concepts = [
12224
+ "validation",
12225
+ "validation-messages",
12226
+ "conditional",
12227
+ "derivation",
12228
+ "property-derivation",
12229
+ "options-format",
12230
+ "expression-variables",
12231
+ "async-validators",
12232
+ "buttons",
12233
+ "external-data"
12234
+ ];
12235
+ if (fieldTypes.includes(key)) return "Field Type";
12236
+ if (containers.includes(key)) return "Container";
12237
+ if (concepts.includes(key)) return "Concept";
12238
+ return "Pattern";
12239
+ }
12240
+ function buildIndex() {
12241
+ if (searchIndex) return searchIndex;
12242
+ const entries = [];
12243
+ const aliasMap = {};
12244
+ for (const [alias, target] of Object.entries(TOPIC_ALIASES)) {
12245
+ if (!aliasMap[target]) aliasMap[target] = [];
12246
+ aliasMap[target].push(alias);
12247
+ }
12248
+ for (const key of Object.keys(TOPICS)) {
12249
+ const topic = TOPICS[key];
12250
+ const description = TOPIC_DESCRIPTIONS[key] || "";
12251
+ const aliases = aliasMap[key] || [];
12252
+ entries.push({
12253
+ id: key,
12254
+ category: "topic",
12255
+ subcategory: getTopicSubcategory(key),
12256
+ description,
12257
+ aliases,
12258
+ content: `${description} ${topic.brief}`.toLowerCase(),
12259
+ toolCall: `ngforge_lookup topic="${key}"`
12260
+ });
12261
+ }
12262
+ for (const [key, pattern] of Object.entries(PATTERNS)) {
12263
+ entries.push({
12264
+ id: key,
12265
+ category: "pattern",
12266
+ subcategory: "Example",
12267
+ description: pattern.description,
12268
+ aliases: [],
12269
+ content: `${pattern.description} ${pattern.brief}`.toLowerCase(),
12270
+ toolCall: `ngforge_examples pattern="${key}"`
12271
+ });
12272
+ }
12273
+ searchIndex = entries;
12274
+ return entries;
12275
+ }
12276
+ function scoreEntry(entry, tokens) {
12277
+ let score = 0;
12278
+ const idLower = entry.id.toLowerCase();
12279
+ for (const token of tokens) {
12280
+ if (idLower === token) {
12281
+ score += 10;
12282
+ } else if (idLower.includes(token)) {
12283
+ score += 6;
12284
+ }
12285
+ for (const alias of entry.aliases) {
12286
+ const aliasLower = alias.toLowerCase();
12287
+ if (aliasLower === token) {
12288
+ score += 8;
12289
+ } else if (aliasLower.includes(token)) {
12290
+ score += 4;
12291
+ }
12292
+ }
12293
+ if (entry.description.toLowerCase().includes(token)) {
12294
+ score += 3;
12295
+ }
12296
+ if (entry.content.includes(token)) {
12297
+ score += 1;
12298
+ }
12299
+ }
12300
+ return score;
12301
+ }
12302
+ function formatResults(results) {
12303
+ if (results.length === 0) {
12304
+ return `# No Results Found
12305
+
12306
+ Try broader terms or check available topics:
12307
+ - \`ngforge_lookup topic="list"\` \u2014 all documentation topics
12308
+ - \`ngforge_examples pattern="list"\` \u2014 all code examples
12309
+
12310
+ **Common searches:** validation, conditional, array, derivation, hidden, options, multi-page`;
12311
+ }
12312
+ const lines = ["# Search Results", ""];
12313
+ for (let i = 0; i < results.length; i++) {
12314
+ const { entry } = results[i];
12315
+ const categoryLabel = entry.category === "topic" ? `Topic \xB7 ${entry.subcategory}` : `Example \xB7 ${entry.subcategory}`;
12316
+ lines.push(`${i + 1}. **${entry.id}** (${categoryLabel})`);
12317
+ lines.push(` ${entry.description}`);
12318
+ lines.push(` \u2192 \`${entry.toolCall}\``);
12319
+ lines.push("");
12320
+ }
12321
+ lines.push("---");
12322
+ lines.push('**Tip:** Use `ngforge_lookup topic="workflow"` for the recommended tool usage guide.');
12323
+ return lines.join("\n");
12324
+ }
12325
+ function registerSearchTool(server) {
12326
+ server.tool(
12327
+ "ngforge_search",
12328
+ `SEARCH: Free-text keyword search across all documentation and examples - "Find topics about X"
12329
+
12330
+ Search by natural language or keywords. Returns matching topics and examples with direct tool calls.
12331
+
12332
+ Examples:
12333
+ - "conditional required" \u2192 finds conditional logic and validation topics
12334
+ - "array template" \u2192 finds simplified-array topic and minimal-array example
12335
+ - "hide field" \u2192 finds conditional visibility documentation
12336
+ - "options dropdown" \u2192 finds select field and options-format topics
12337
+
12338
+ Use this when you don't know the exact topic name.
12339
+
12340
+ New to ng-forge? Start with: ngforge_lookup topic="workflow"`,
12341
+ {
12342
+ query: z58.string().describe("Natural language or keywords to search for")
12343
+ },
12344
+ async ({ query }) => {
12345
+ const index = buildIndex();
12346
+ const tokens = query.toLowerCase().split(/[\s,;.!?]+/).filter((t) => t.length > 0);
12347
+ if (tokens.length === 0) {
12348
+ return {
12349
+ content: [
12350
+ {
12351
+ type: "text",
12352
+ text: formatResults([])
12353
+ }
12354
+ ]
12355
+ };
12356
+ }
12357
+ const scored = index.map((entry) => ({ entry, score: scoreEntry(entry, tokens) })).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, 10);
12358
+ return {
12359
+ content: [{ type: "text", text: formatResults(scored) }]
12360
+ };
12361
+ }
12362
+ );
12363
+ }
12364
+
11719
12365
  // packages/dynamic-form-mcp/src/tools/index.ts
11720
12366
  function registerTools(server) {
11721
12367
  registerLookupTool(server);
11722
12368
  registerExamplesTool(server);
11723
12369
  registerValidateTool(server);
11724
12370
  registerScaffoldTool(server);
12371
+ registerSearchTool(server);
11725
12372
  }
11726
12373
 
11727
12374
  // packages/dynamic-form-mcp/src/server.ts