Advanced Topics Custom Integrations

Custom Integrations

Create custom UI integrations for ng-forge dynamic forms using any component library or design system.

Package Structure

The @ng-forge/dynamic-forms package is organized into multiple entrypoints to keep the core abstract and provide specialized utilities for integration authors:

EntrypointPurpose
@ng-forge/dynamic-formsCore types, components, and configuration (for all users)
@ng-forge/dynamic-forms/integrationField types, mappers, and utilities for UI library authors
@ng-forge/dynamic-forms/testingTesting utilities for unit testing custom integrations

When building a custom integration, you'll primarily import from the /integration entrypoint:

// Core types (used by everyone)
import { DynamicForm, provideDynamicForm, FormConfig, DynamicText } from '@ng-forge/dynamic-forms';

// Integration utilities (for UI library authors)
import {
  InputField,
  SelectField,
  CheckboxField,
  valueFieldMapper,
  checkboxFieldMapper,
  createResolvedErrorsSignal,
} from '@ng-forge/dynamic-forms/integration';

Integration Overview

UI integrations map field types to your components using FieldTypeDefinition objects. Each definition specifies the field type name, component loader, and mapper function.

Basic Steps

1. Define Field Type Interface

Create a type interface extending the base field type:

import { ValueFieldComponent, DynamicText } from '@ng-forge/dynamic-forms';
import { InputField } from '@ng-forge/dynamic-forms/integration';

// Define your custom props
export interface CustomInputProps extends Record<string, unknown> {
  appearance?: 'outline' | 'fill';
  hint?: DynamicText;
  type?: 'text' | 'email' | 'password' | 'number';
}

// Extend the base InputField with your props
export type CustomInputField = InputField<CustomInputProps>;

// Define the component interface (used for type checking)
export type CustomInputComponent = ValueFieldComponent<CustomInputField>;

2. Create Field Component

Implement the component using Angular's signal forms:

import { Component, input } from '@angular/core';
import { Field, FieldTree } from '@angular/forms/signals';
import { DynamicText, DynamicTextPipe } from '@ng-forge/dynamic-forms';
import { CustomInputComponent, CustomInputProps } from './custom-input.type';
import { AsyncPipe } from '@angular/common';

@Component({
  selector: 'custom-input',
  imports: [Field, DynamicTextPipe, AsyncPipe],
  template: `
    @let f = field();

    <div class="custom-field" [class.custom-outline]="props()?.appearance === 'outline'">
      @if (label()) {
      <label [for]="key() + '-input'">{{ label() | dynamicText | async }}</label>
      }

      <input
        [id]="key() + '-input'"
        [field]="f"
        [type]="props()?.type || 'text'"
        [placeholder]="(placeholder() | dynamicText | async) ?? ''"
        [disabled]="f().disabled()"
        [attr.tabindex]="tabIndex()"
      />

      @if (props()?.hint; as hint) {
      <div class="hint">{{ hint | dynamicText | async }}</div>
      } @if (f().touched() && f().invalid()) {
      <div class="error">{{ f().errors() | json }}</div>
      }
    </div>
  `,
  host: {
    '[id]': '`${key()}`',
    '[class]': 'className()',
  },
})
export default class CustomInputFieldComponent implements CustomInputComponent {
  // Required inputs
  readonly field = input.required<FieldTree<string>>();
  readonly key = input.required<string>();

  // Standard inputs
  readonly label = input<DynamicText>();
  readonly placeholder = input<DynamicText>();
  readonly className = input<string>('');
  readonly tabIndex = input<number>();

  // Custom props
  readonly props = input<CustomInputProps>();
}

3. Create Field Type Definition

Define the field type registration:

import { FieldTypeDefinition } from '@ng-forge/dynamic-forms';
import { valueFieldMapper } from '@ng-forge/dynamic-forms/integration';

export const CustomInputType: FieldTypeDefinition = {
  name: 'input',
  loadComponent: () => import('./custom-input.component'),
  mapper: valueFieldMapper,
};

4. Create Provider Function

Export a function that returns all your field type definitions:

import { FieldTypeDefinition } from '@ng-forge/dynamic-forms';
import { CustomInputType } from './fields/input';
import { CustomSelectType } from './fields/select';
import { CustomCheckboxType } from './fields/checkbox';

export function withCustomFields(): FieldTypeDefinition[] {
  return [
    CustomInputType,
    CustomSelectType,
    CustomCheckboxType,
    // ... more field types
  ];
}

5. Configure App

Add your fields to the app configuration:

app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideDynamicForm } from '@ng-forge/dynamic-forms';
import { withCustomFields } from './custom-fields';

export const appConfig: ApplicationConfig = {
  providers: [provideDynamicForm(...withCustomFields())],
};

Component Interface Types

ng-forge provides component interface types for different field categories:

ValueFieldComponent

For fields that collect user input (input, select, textarea, datepicker, radio, slider):

import { ValueFieldComponent } from '@ng-forge/dynamic-forms';
import { InputField } from '@ng-forge/dynamic-forms/integration';

export type CustomInputComponent = ValueFieldComponent<CustomInputField>;

The component must implement these inputs:

  • field: FieldTree<TValue> - The form field from Angular's signal forms
  • key: string - Unique field identifier
  • label?: DynamicText - Field label
  • placeholder?: DynamicText - Placeholder text
  • className?: string - CSS classes
  • tabIndex?: number - Tab order
  • props?: TProps - Custom field-specific props

CheckedFieldComponent

For checkbox and toggle fields:

import { CheckedFieldComponent } from '@ng-forge/dynamic-forms';
import { CheckboxField } from '@ng-forge/dynamic-forms/integration';

export type CustomCheckboxComponent = CheckedFieldComponent<CustomCheckboxField>;

Similar to ValueFieldComponent but specifically for boolean checkbox fields.

Field Binding with [field]

The key to connecting your component to Angular's signal forms is the [field] binding. Import Field and FieldTree from Angular's signal forms package:

import { Field, FieldTree } from '@angular/forms/signals';

Then use the [field] directive on form controls:

<input [field]="f" ... />
<mat-checkbox [field]="f" ... />
<select [field]="f" ... />

This directive automatically:

  • Binds the form control value
  • Handles value changes
  • Manages validation state
  • Syncs disabled/readonly states

Field Mappers

Mappers convert field definitions to component input bindings. ng-forge provides built-in mappers:

valueFieldMapper

For standard value-bearing fields:

import { FieldTypeDefinition } from '@ng-forge/dynamic-forms';
import { valueFieldMapper } from '@ng-forge/dynamic-forms/integration';

export const CustomInputType: FieldTypeDefinition = {
  name: 'input',
  loadComponent: () => import('./custom-input.component'),
  mapper: valueFieldMapper, // Maps value fields
};

checkboxFieldMapper

For checkbox/toggle fields:

import { FieldTypeDefinition } from '@ng-forge/dynamic-forms';
import { checkboxFieldMapper } from '@ng-forge/dynamic-forms/integration';

export const CustomCheckboxType: FieldTypeDefinition = {
  name: 'checkbox',
  loadComponent: () => import('./custom-checkbox.component'),
  mapper: checkboxFieldMapper, // Maps checkbox fields
};

Custom Mappers

For specialized fields (like buttons), create custom mappers:

import { Binding, inputBinding } from '@angular/core';
import { FieldTypeDefinition } from '@ng-forge/dynamic-forms';
import { ButtonField } from '@ng-forge/dynamic-forms/integration';

export function buttonFieldMapper(fieldDef: ButtonField<unknown, unknown>): Binding[] {
  return [
    inputBinding('key', () => fieldDef.key),
    inputBinding('label', () => fieldDef.label),
    inputBinding('disabled', () => fieldDef.disabled ?? false),
    inputBinding('event', () => fieldDef.event),
    inputBinding('props', () => fieldDef.props),
    inputBinding('className', () => fieldDef.className),
  ];
}

export const CustomButtonType: FieldTypeDefinition = {
  name: 'button',
  loadComponent: () => import('./custom-button.component'),
  mapper: buttonFieldMapper,
  valueHandling: 'exclude', // Buttons don't contribute to form value
};

Value Handling

The valueHandling property controls whether a field contributes to the form value:

  • 'include' (default) - Field value included in form data
  • 'exclude' - Field excluded from form data (for buttons, text fields, etc.)
export const ButtonType: FieldTypeDefinition = {
  name: 'button',
  loadComponent: () => import('./button.component'),
  mapper: buttonFieldMapper,
  valueHandling: 'exclude', // Buttons don't have values
};

Type Safety with Module Augmentation

Register your field types with TypeScript for full type inference:

// In your field types file
declare module '@ng-forge/dynamic-forms' {
  interface FieldRegistryLeaves {
    input: CustomInputField;
    select: CustomSelectField;
    checkbox: CustomCheckboxField;
  }
}

This enables:

  • IntelliSense for field properties
  • Type checking in form configurations
  • Compile-time validation of field definitions

Best Practices

Use proper component interfaces:

Leverage [field] binding:

  • Use [field]="f" on form controls
  • Automatic value and validation handling
  • No manual form control management needed

Support DynamicText:

Handle validation state:

  • Show errors when f().touched() && f().invalid()
  • Display validation messages from f().errors()
  • Clear, accessible error presentation

Accessibility:

  • Proper ARIA attributes
  • Keyboard navigation
  • Focus management
  • Screen reader support

Lazy loading:

  • Use dynamic imports in loadComponent
  • Keeps initial bundle size small
  • Components load on-demand

Reference Implementations

See complete integrations:

The Material integration source code is the most comprehensive example of implementing custom field types.