Skip to main content

2. Bind controls

[formNode] binds naturally to native elements, signal custom controls, Angular Material, PrimeNG, and other controls built on Angular's standard forms contracts. There are no Form Nodes adapters to install, wrappers to write, or per-library providers to configure. Import the control as its own documentation requires, then bind your node with the same [formNode] syntax.

🔌 Bind native controls​

Expand the initial name binding to every field with [formNode]. The same directive also binds radio buttons, selects, checkboxes, and textareas:

Native controls and their model
import { Component } from '@angular/core';
import { field, form, FormNodeDirective } from '@ngblocks/form-nodes';

@Component({
selector: 'app-profile-editor',
imports: [FormNodeDirective],
template: `
<!-- Text input binding -->
<label>
Name
<input [formNode]="form.name" />
</label>

<!-- Number input binding -->
<label>
Age
<input type="number" [formNode]="form.age" />
</label>

<!-- Email input binding -->
<label>
Email
<input type="email" [formNode]="form.email" />
</label>

<!-- Radio group: multiple options, one field -->
<fieldset>
<legend>Preferred contact method</legend>

<label>
<input type="radio" value="email" [formNode]="form.contactMethod" />
Email
</label>

<label>
<input type="radio" value="phone" [formNode]="form.contactMethod" />
Phone
</label>
</fieldset>

<!-- Select binding -->
<label>
Country
<select [formNode]="form.country">
<option value="CH">Switzerland</option>
<option value="ES">Spain</option>
</select>
</label>

<!-- Checkbox binding -->
<label>
<input type="checkbox" [formNode]="form.newsletter" />
Receive the newsletter
</label>

<!-- Textarea binding -->
<label>
About you
<textarea rows="3" [formNode]="form.bio"></textarea>
</label>

<!-- Read the current field values -->
<p>Current name: {{ form.name() }}</p>
<p>Current age: {{ form.age() }}</p>
<p>Current email: {{ form.email() }}</p>
<p>Preferred contact method: {{ form.contactMethod() }}</p>
<p>Country code: {{ form.country() }}</p>
<p>Newsletter enabled: {{ form.newsletter() }}</p>
<p>Bio: {{ form.bio() }}</p>
`,
})
export class ProfileEditor {
form = form({
name: field(''),
age: field<number>(null),
email: field(''),
contactMethod: field('email'),
country: field('CH'),
newsletter: field.strict(false),
bio: field(''),
});
}

The directive handles both directions:

  • Programmatic set() calls update the rendered control.
  • User input updates the field and aggregated form value.
  • User input marks the field dirty.
  • Blur marks it touched.
  • Numeric inputs produce numbers rather than raw strings.
  • Radio buttons bound to the same field share a generated name. Each option has its own string value; selecting Phone writes 'phone' to form.contactMethod().
  • A <select> with string-valued options writes the selected option value, such as 'CH'.
  • A checkbox writes true or false; field.strict(false) keeps this field non-nullable.
  • A <textarea> reads and writes text just like a text input.

In the radio group, field('email') initially selects Email. Bind both options to form.contactMethod and let [formNode] manage name and checked. The fieldset, legend, and labels give the group and its options accessible names. See Radio buttons for a focused example.

The model remains the source of truth; no FormControl, formControlName, or string path is required.

The same [formNode] binding works across Angular's common control contracts.

🔌 Bind signal custom controls naturally​

A custom component can expose Angular's standard model() value contract:

import { Component, model } from '@angular/core';

@Component({
selector: 'app-rating',
template: `
<button type="button" (click)="value.set(1)">1</button>
<button type="button" (click)="value.set(2)">2</button>
<button type="button" (click)="value.set(3)">3</button>
`,
})
export class RatingControl {
readonly value = model<number | null>(null);
}

Bind it exactly like a native input:

@Component({
imports: [FormNodeDirective, RatingControl],
template: `<app-rating [formNode]="form.rating" />`,
})
export class ReviewEditor {
form = form({
rating: field<number>(null),
});
}

No Form Nodes-specific interface or provider is required for the conventional value = model() shape.

🔌 Bind a custom ControlValueAccessor​

A component registered through Angular's NG_VALUE_ACCESSOR token uses the same [formNode] binding. The control implements ControlValueAccessor; Form Nodes connects its callbacks to the field automatically:

Custom control and parent component
import { Component, forwardRef, signal } from '@angular/core';
import { field, form, FormNodeDirective } from '@ngblocks/form-nodes';
import { NG_VALUE_ACCESSOR, type ControlValueAccessor } from '@angular/forms';

@Component({
selector: 'app-text-input',
template: `
<label>
Name
<input
#input
[value]="value()"
[disabled]="disabled()"
(input)="changeValue(input.value)"
(blur)="onTouched()"
/>
</label>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TextInputControl),
multi: true,
}],
})
export class TextInputControl implements ControlValueAccessor {
value = signal('');

disabled = signal(false);

onChange: (value: string) => void = () => {};

onTouched: () => void = () => {};

writeValue(value: string | null) {
this.value.set(value ?? '');
}

registerOnChange(callback: (value: string) => void) {
this.onChange = callback;
}

registerOnTouched(callback: () => void) {
this.onTouched = callback;
}

setDisabledState(disabled: boolean) {
this.disabled.set(disabled);
}

changeValue(value: string) {
this.value.set(value);
this.onChange(value);
}
}

@Component({
selector: 'app-profile-editor',
imports: [FormNodeDirective, TextInputControl],
template: `
<app-text-input [formNode]="form.name" />
<p>Current name: {{ form.name() }}</p>
<p>Touched: {{ form.name.touched() }}</p>
`,
})
export class ProfileEditor {
form = form({
name: field('Ada'),
});
}
  • writeValue() displays programmatic field changes. It must not call onChange().
  • The control calls the registered onChange() callback for user input; this updates the field and marks it dirty.
  • Calling the registered onTouched() callback on blur marks the field touched.
  • setDisabledState() receives the field's disabled state and applies it to the inner input.

NG_VALUE_ACCESSOR is the standard Angular provider for the custom control. The parent only imports FormNodeDirective and the control component; no ngModel, FormControl, or additional Form Nodes provider is needed. The same component can still be used with Angular's other forms APIs.

See ControlValueAccessor in the custom-controls guide for accessor selection, validation integration, and other supported contracts.

🔌 Bind Angular Material controls naturally​

No Angular Material-specific Form Nodes integration is required. After installing Material, import its component modules normally and place [formNode] directly on controls that implement Angular Forms APIs. For example, mat-select can replace a native country select without changing the node or introducing a FormControl:

import { Component } from '@angular/core';
import { MatSelectModule } from '@angular/material/select';
import { field, FormNodeDirective, form } from '@ngblocks/form-nodes';
import { MatFormFieldModule } from '@angular/material/form-field';

@Component({
imports: [FormNodeDirective, MatFormFieldModule, MatSelectModule],
template: `
<mat-form-field>
<mat-label>Country</mat-label>
<mat-select [formNode]="form.country">
<mat-option value="CH">Switzerland</mat-option>
<mat-option value="ES">Spain</mat-option>
</mat-select>
</mat-form-field>
`,
})
export class CountryEditor {
form = form({
country: field('CH'),
});
}

See the official Angular Material select documentation for its ordinary installation and theming requirements. Form Nodes requires no additional Material setup. Continue with the complete Angular Material integration for inputs, selects, checkboxes, datepickers, errors, submission, and testing.

🔌 Bind PrimeNG controls naturally​

PrimeNG also needs no Form Nodes adapter or wrapper. Import its module normally and bind p-select directly; its Angular Forms compatibility supplies the ControlValueAccessor contract that [formNode] recognizes:

import { Component } from '@angular/core';
import { SelectModule } from 'primeng/select';
import { field, FormNodeDirective, form } from '@ngblocks/form-nodes';

@Component({
imports: [FormNodeDirective, SelectModule],
template: `
<p-select
[formNode]="form.city"
[options]="cities"
placeholder="Select a city"
/>
`,
})
export class CityEditor {
cities = ['Madrid', 'Zurich', 'London'];

form = form({
city: field<string>(null),
});
}

See the official PrimeNG Select documentation for package setup and available options. Once PrimeNG itself is configured, there is no extra Form Nodes configuration. Continue with the complete PrimeNG integration for installation, inputs, selects, checkboxes, datepickers, validation styling, submission, and testing.

🔌 Bind other Angular-compatible controls​

In general, use [formNode] with native elements, value = model() or checked = model() custom controls, and components implementing ControlValueAccessor. This is why established Angular component libraries work without library-specific support in Form Nodes: the integration is based on Angular's contracts rather than component brand names. Form Nodes discovers the appropriate mechanism automatically.

Based on their documented Angular Forms support, controls from these well-known libraries are also expected to bind naturally:

LibraryRepresentative compatible controlWhy it is expected to work
Angular Materialmat-selectSupports Angular Forms and participates through NgControl
PrimeNGp-selectSupports template-driven and reactive Angular forms
Kendo UI for Angularkendo-dropdownlistDocuments both ngModel and reactive-forms binding
NG-ZORROnz-selectExposes ngModel and SelectControlValueAccessor-compatible semantics
Taiga UIInput controlsUse native inputs and/or follow Angular form-control state

For example, a compatible Kendo or NG-ZORRO control uses the same binding shape:

<kendo-dropdownlist [formNode]="form.country" [data]="countries" />

<nz-select [formNode]="form.country">
<nz-option nzValue="CH" nzLabel="Switzerland" />
<nz-option nzValue="ES" nzLabel="Spain" />
</nz-select>

This is a contract-based compatibility expectation, not a claim that every component in every suite is a form control. Check that the particular component supports ngModel, reactive forms, or ControlValueAccessor; display-only components do not have a value contract to bind. For components with unusual integration requirements, see the advanced custom-controls guide.

The Advanced custom controls guide documents the complete compatibility matrix, state inputs, hooks, precedence, and limitations.

Continue with Step 3: Add validation.