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:
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 stringvalue; selecting Phone writes'phone'toform.contactMethod(). - A
<select>with string-valued options writes the selected option value, such as'CH'. - A checkbox writes
trueorfalse;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:
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 callonChange().- 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:
| Library | Representative compatible control | Why it is expected to work |
|---|---|---|
| Angular Material | mat-select | Supports Angular Forms and participates through NgControl |
| PrimeNG | p-select | Supports template-driven and reactive Angular forms |
| Kendo UI for Angular | kendo-dropdownlist | Documents both ngModel and reactive-forms binding |
| NG-ZORRO | nz-select | Exposes ngModel and SelectControlValueAccessor-compatible semantics |
| Taiga UI | Input controls | Use 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.
🔗 Related guides and reference​
- Control binding details native value conversion, constraints, focus, status classes, SSR, and hydration.
- Custom controls introduces signal model components and
existing
ControlValueAccessorcontrols. - Node API lists the public API available from a
viewChild()binding. - Build a custom rating control is a focused end-to-end recipe.
Continue with Step 3: Add validation.