@ng-forge/dynamic-form-mcp 0.5.1 → 0.5.2

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.
@@ -9,7 +9,7 @@
9
9
  "id": "advanced-events",
10
10
  "title": "Advanced Events",
11
11
  "category": "advanced",
12
- "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### AddArrayItemEvent\n\nAdd an item to an array field.\n\n```typescript\n// Add item using array's template\neventBus.dispatch(AddArrayItemEvent, 'contacts');\n\n// Add item with custom field template\nconst fieldTemplate = { key: 'email', type: 'input', value: '' };\neventBus.dispatch(AddArrayItemEvent, 'contacts', fieldTemplate);\n\n// Add item at specific index\neventBus.dispatch(AddArrayItemEvent, 'contacts', fieldTemplate, 2);\n```\n\n**Constructor parameters:**\n\n- `arrayKey: string` - Key of the array field\n- `field?: ArrayAllowedChildren` - Optional field template (uses array's template if omitted)\n- `index?: number` - Optional index to insert at (defaults to end)\n\n### RemoveArrayItemEvent\n\nRemove an item from an array field.\n\n```typescript\n// Remove item at index 1\neventBus.dispatch(RemoveArrayItemEvent, 'contacts', 1);\n\n// Remove last item\neventBus.dispatch(RemoveArrayItemEvent, 'contacts');\n```\n\n**Constructor parameters:**\n\n- `arrayKey: string` - Key of the array field\n- `index?: number` - Index to remove (defaults to last item)\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// ✓ Type-safe event subscription\nclass UserUpdatedEvent implements FormEvent {\n readonly type = 'user-updated' as const;\n constructor(public readonly userId: string) {}\n}\n\n// ✓ 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// ✗ 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```"
12
+ "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** → creates a **primitive item** (field value extracted directly)\n- **Array of FieldDefs** → 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// ✓ Type-safe event subscription\nclass UserUpdatedEvent implements FormEvent {\n readonly type = 'user-updated' as const;\n constructor(public readonly userId: string) {}\n}\n\n// ✓ 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// ✗ 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```"
13
13
  },
14
14
  {
15
15
  "id": "advanced-expression-parser",
@@ -27,7 +27,7 @@
27
27
  "id": "ai-integration",
28
28
  "title": "AI Integration",
29
29
  "category": "ai-integration",
30
- "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 | ✅ |\n| `ngforge_examples` | Get working code examples for common form patterns | ✅ |\n| `ngforge_validate` | Validate FormConfig and get detailed error feedback | ✅ |\n| `ngforge_scaffold` | Generate valid FormConfig skeletons | ✅ |\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`, `removeArrayItem` |\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)."
30
+ "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 | ✅ |\n| `ngforge_examples` | Get working code examples for common form patterns | ✅ |\n| `ngforge_validate` | Validate FormConfig and get detailed error feedback | ✅ |\n| `ngforge_scaffold` | Generate valid FormConfig skeletons | ✅ |\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)."
31
31
  },
32
32
  {
33
33
  "id": "dynamic-behavior-conditional-logic-overview",
@@ -51,7 +51,13 @@
51
51
  "id": "dynamic-behavior-value-derivation-basics",
52
52
  "title": "Dynamic Behavior Value Derivation Basics",
53
53
  "category": "dynamic-behavior",
54
- "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 ─┐\n ├── subtotal ─┐\nunitPrice ─┘ ├── total\n │\n tax ─────────┘\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- **[Conditional Logic](../conditional-logic/overview/)** - Control field visibility and state\n- **[Array Fields](../../prebuilt/form-arrays/)** - Working with array fields\n- **[Examples](/docs/examples)** - Real-world form patterns"
54
+ "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 ─┐\n ├── subtotal ─┐\nunitPrice ─┘ ├── total\n │\n tax ─────────┘\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) - Derive component properties (minDate, options, label) from form values\n- **[Conditional Logic](../conditional-logic/overview/)** - Control field visibility and state\n- **[Array Fields](../../prebuilt/form-arrays/)** - Working with array fields\n- **[Examples](/docs/examples)** - Real-world form patterns"
55
+ },
56
+ {
57
+ "id": "dynamic-behavior-value-derivation-basics-property-derivation",
58
+ "title": "Dynamic Behavior Value Derivation Basics Property Derivation",
59
+ "category": "dynamic-behavior",
60
+ "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: 'propertyDerivation'` 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: 'propertyDerivation',\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## 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: 'propertyDerivation',\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: 'propertyDerivation',\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 propertyDerivations: {\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: 'propertyDerivation',\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: 'propertyDerivation',\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: 'propertyDerivation',\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: 'propertyDerivation',\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: 'propertyDerivation',\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: 'propertyDerivation',\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: 'propertyDerivation',\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 propertyDerivations: {\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: 'propertyDerivation',\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: 'propertyDerivation',\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: 'propertyDerivation',\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: 'propertyDerivation',\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: 'propertyDerivation'` |\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.propertyDerivations` |\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: 'propertyDerivation',\n debugName: 'endDate minDate constraint',\n targetProperty: 'minDate',\n expression: 'formValue.startDate',\n },\n];\n```\n\n## PropertyDerivationLogicConfig Interface\n\n```typescript\ninterface PropertyDerivationLogicConfig {\n /** Logic type identifier */\n type: 'propertyDerivation';\n\n /** Property to set on the field component */\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 propertyDerivations: {\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: 'propertyDerivation',\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/)** - Working with array fields"
55
61
  },
56
62
  {
57
63
  "id": "age-conditional-form",
@@ -123,13 +129,13 @@
123
129
  "id": "form-arrays",
124
130
  "title": "Form Arrays",
125
131
  "category": "prebuilt",
126
- "content": "Arrays create dynamic collections of field values. The fields array defines a **template** that is cloned for each array item.\n\n## Interactive Demo\n\n<iframe src=\"http://localhost:4201/#/examples/array-field\" class=\"example-frame\" title=\"Array Field Demo\"></iframe>\n\n## Flat Arrays (Primitive Values)\n\nFor simple arrays of primitive values, use a leaf field as the template:\n\n```typescript\n{\n key: 'tags',\n type: 'array',\n fields: [\n { key: 'tag', type: 'input', label: 'Tag', value: '' }\n ],\n}\n```\n\nThis creates a flat array in the form value:\n\n```typescript\n{\n tags: ['tag1', 'tag2', 'tag3'];\n}\n```\n\n## Object Arrays (Nested Groups)\n\nFor arrays of objects, use a group field as the template:\n\n```typescript\n{\n key: 'contacts',\n type: 'array',\n fields: [{\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\nThis creates an array of objects:\n\n```typescript\n{\n contacts: [\n { name: 'John Doe', phone: '5551234567' },\n { name: 'Jane Smith', phone: '5559876543' },\n ];\n}\n```\n\n## Array vs Group\n\n- **Groups** create nested objects with keys: `{ address: { street: '', city: '' } }`\n- **Flat Arrays** create lists of values: `{ tags: ['value1', 'value2'] }`\n- **Object Arrays** create lists of objects: `{ items: [{name: ''}, {name: ''}] }`\n\n## Dynamic Add/Remove\n\nArray items can be added or removed dynamically at runtime using the event bus:\n\n```typescript\nimport { EventBus, AddArrayItemEvent, RemoveArrayItemEvent } from '@ng-forge/dynamic-forms';\n\n// Inject the event bus\neventBus = inject(EventBus);\n\n// Add item to array\naddItem() {\n this.eventBus.dispatch(AddArrayItemEvent, 'tags');\n}\n\n// Add item at specific index\naddItemAt(index: number) {\n this.eventBus.dispatch(AddArrayItemEvent, 'tags', index);\n}\n\n// Remove last item\nremoveItem() {\n this.eventBus.dispatch(RemoveArrayItemEvent, 'tags');\n}\n\n// Remove item at specific index\nremoveItemAt(index: number) {\n this.eventBus.dispatch(RemoveArrayItemEvent, 'tags', 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: Flat Array\n\nHere's a complete working example of a flat array field with dynamic add/remove:\n\n```typescript\nimport { Component, inject } from '@angular/core';\nimport { DynamicForm, EventBus, AddArrayItemEvent, RemoveArrayItemEvent } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-tags-form',\n imports: [DynamicForm],\n template: `\n <form [dynamic-form]=\"formConfig\"></form>\n <button (click)=\"addTag()\">Add Tag</button>\n `,\n})\nexport class TagsFormComponent {\n private eventBus = inject(EventBus);\n\n formConfig = {\n fields: [\n // Note: Array fields don't support the 'label' property.\n // Use a text field above if you need a section header.\n {\n key: 'tags',\n type: 'array',\n fields: [\n {\n key: 'tag',\n type: 'input',\n label: 'Tag',\n value: '',\n required: true,\n minLength: 2,\n },\n ],\n },\n ],\n };\n\n addTag() {\n this.eventBus.dispatch(AddArrayItemEvent, 'tags');\n }\n\n removeTag(index: number) {\n this.eventBus.dispatch(RemoveArrayItemEvent, 'tags', index);\n }\n}\n```\n\n## Complete Example: Object Array\n\nHere's a complete working example of an object array field with validation:\n\n```typescript\nimport { Component, inject } from '@angular/core';\nimport { DynamicForm, EventBus, AddArrayItemEvent, RemoveArrayItemEvent } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-contacts-form',\n imports: [DynamicForm],\n template: `\n <form [dynamic-form]=\"formConfig\"></form>\n <button (click)=\"addContact()\">Add Contact</button>\n `,\n})\nexport class ContactsFormComponent {\n private eventBus = inject(EventBus);\n\n formConfig = {\n fields: [\n // Note: Array fields don't support the 'label' property.\n // Use a text field above if you need a section header.\n {\n key: 'contacts',\n type: 'array',\n fields: [\n {\n type: 'group',\n fields: [\n {\n key: 'name',\n type: 'input',\n label: 'Contact Name',\n value: '',\n required: true,\n minLength: 2,\n },\n {\n key: 'phone',\n type: 'input',\n label: 'Phone Number',\n value: '',\n required: true,\n pattern: /^\\d{10}$/,\n },\n {\n key: 'relationship',\n type: 'select',\n label: 'Relationship',\n value: 'friend',\n options: [\n { label: 'Family', value: 'family' },\n { label: 'Friend', value: 'friend' },\n { label: 'Colleague', value: 'colleague' },\n ],\n },\n ],\n },\n ],\n },\n ],\n };\n\n addContact() {\n this.eventBus.dispatch(AddArrayItemEvent, 'contacts');\n }\n\n removeContact(index: number) {\n this.eventBus.dispatch(RemoveArrayItemEvent, 'contacts', index);\n }\n}\n```\n\n## Template Field Concept\n\nThe key concept is that the `fields` array contains a **template** (typically one field definition):\n\n- **Flat arrays**: Template is a leaf field → creates `[value1, value2]`\n- **Object arrays**: Template is a group field → creates `[{...}, {...}]`\n\nWhen you add an array item via the event bus, the template is cloned and a new instance is created with the appropriate default value.\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 (as template)\n\nArrays can use these field types as templates:\n\n- Leaf fields (input, select, checkbox, etc.) → creates flat arrays\n- Group fields → creates object arrays\n- Row fields → for horizontal layouts within each array item\n\nSee [Type Safety & Inference](../advanced/type-safety/basics) for details on how arrays affect type inference."
132
+ "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## 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) → **Primitive item** - extracts field value directly\n- **Array of FieldDefs** → **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## 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 dynamic add/remove:\n\n```typescript\nimport { Component, inject } from '@angular/core';\nimport { DynamicForm, EventBus, arrayEvent } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-tags-form',\n imports: [DynamicForm],\n template: `\n <form [dynamic-form]=\"formConfig\"></form>\n <button (click)=\"addTag()\">Add Tag</button>\n `,\n})\nexport class TagsFormComponent {\n private eventBus = inject(EventBus);\n\n // Template for new tags - single field (primitive item)\n private tagTemplate = {\n key: 'tag',\n type: 'input',\n label: 'Tag',\n value: '',\n required: true,\n minLength: 2,\n };\n\n formConfig = {\n fields: [\n {\n key: 'tags',\n type: 'array',\n // Start with two initial tags (primitive items - not wrapped in arrays)\n fields: [\n { key: 'tag', type: 'input', label: 'Tag', value: 'featured', required: true, minLength: 2 },\n { key: 'tag', type: 'input', label: 'Tag', value: 'popular', required: true, minLength: 2 },\n ],\n },\n ],\n };\n\n // Form value: { tags: ['featured', 'popular'] }\n\n addTag() {\n this.eventBus.dispatch(arrayEvent('tags').append(this.tagTemplate));\n }\n\n removeTag(index: number) {\n this.eventBus.dispatch(arrayEvent('tags').removeAt(index));\n }\n}\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 and validation:\n\n```typescript\nimport { Component, inject } from '@angular/core';\nimport { DynamicForm, EventBus, arrayEvent } from '@ng-forge/dynamic-forms';\n\n@Component({\n selector: 'app-contacts-form',\n imports: [DynamicForm],\n template: `\n <form [dynamic-form]=\"formConfig\"></form>\n <button (click)=\"addContact()\">Add Contact</button>\n `,\n})\nexport class ContactsFormComponent {\n private eventBus = inject(EventBus);\n\n // Template for new contacts\n private contactTemplate = [\n { key: 'name', type: 'input', label: 'Contact Name', value: '', required: true, minLength: 2 },\n { key: 'phone', type: 'input', label: 'Phone Number', value: '', required: true, pattern: /^\\d{10}$/ },\n {\n key: 'relationship',\n type: 'select',\n label: 'Relationship',\n value: 'friend',\n options: [\n { label: 'Family', value: 'family' },\n { label: 'Friend', value: 'friend' },\n { label: 'Colleague', value: 'colleague' },\n ],\n },\n ];\n\n formConfig = {\n fields: [\n {\n key: 'contacts',\n type: 'array',\n // Start with one pre-filled contact\n fields: [\n [\n { key: 'name', type: 'input', label: 'Contact Name', value: 'John Doe', required: true, minLength: 2 },\n { key: 'phone', type: 'input', label: 'Phone Number', value: '5551234567', required: true, pattern: /^\\d{10}$/ },\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 ],\n },\n ],\n };\n\n // Form value: { contacts: [{ name: 'John Doe', phone: '5551234567', relationship: 'family' }] }\n\n addContact() {\n this.eventBus.dispatch(arrayEvent('contacts').append(this.contactTemplate));\n }\n\n removeContact(index: number) {\n this.eventBus.dispatch(arrayEvent('contacts').removeAt(index));\n }\n}\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."
127
133
  },
128
134
  {
129
135
  "id": "form-groups",
130
136
  "title": "Form Groups",
131
137
  "category": "prebuilt",
132
- "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-field\" 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."
138
+ "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."
133
139
  },
134
140
  {
135
141
  "id": "form-pages",
@@ -141,7 +147,7 @@
141
147
  "id": "form-rows",
142
148
  "title": "Form Rows",
143
149
  "category": "prebuilt",
144
- "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-field\" 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)"
150
+ "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)"
145
151
  },
146
152
  {
147
153
  "id": "hidden-fields",
@@ -1 +1 @@
1
- {"version":3,"file":"lookup-topics.d.ts","sourceRoot":"","sources":["../../../../../../packages/dynamic-form-mcp/src/tools/data/lookup-topics.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA+sDlE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA+GhD,CAAC"}
1
+ {"version":3,"file":"lookup-topics.d.ts","sourceRoot":"","sources":["../../../../../../packages/dynamic-form-mcp/src/tools/data/lookup-topics.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAm4DlE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAwHhD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"examples.tool.d.ts","sourceRoot":"","sources":["../../../../../packages/dynamic-form-mcp/src/tools/examples.tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAkpDpE,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAsI5D"}
1
+ {"version":3,"file":"examples.tool.d.ts","sourceRoot":"","sources":["../../../../../packages/dynamic-form-mcp/src/tools/examples.tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA0jEpE,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAsI5D"}
@@ -1 +1 @@
1
- {"version":3,"file":"lookup.tool.d.ts","sourceRoot":"","sources":["../../../../../packages/dynamic-form-mcp/src/tools/lookup.tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAkJpE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAmH1D"}
1
+ {"version":3,"file":"lookup.tool.d.ts","sourceRoot":"","sources":["../../../../../packages/dynamic-form-mcp/src/tools/lookup.tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAmJpE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAmH1D"}