Skip to main content

Custom control contracts

These types describe custom Angular components that bind through [formNode]. They provide compile-time checks for the supported model and optional UI members. They are not constructors, providers, or a replacement for the directive's runtime adapter discovery.

Choose the contract

Component or integrationPublic type
Component with a value modelFormNodeValueControl<TValue, TNode>
Component with a boolean checked modelFormNodeCheckboxControl<TNode>
Generic code accepting either model shapeFormNodeControl<TValue, TNode>
Shared optional state inputs and hooksFormNodeUiControl<TValue, TNode>
The directive attached to a hostFormNodeBinding<TNode> or FormNodeDirective<TNode>
Read-only state across supported binding APIsControlState<TValue>, returned by useFormNodeState()
A native form submission outputFormNodeSubmitEvent<TNode>

Value and checkbox components

A value control exposes value: ModelSignal<TValue>. A checkbox control exposes checked: ModelSignal<boolean>. The contracts reserve the other model property so that the component declares one transport. Match nullability to the node: the following components accept non-nullable values and bind to field.strict() declarations.

profile-controls.ts
import { Component, input, model, output } from '@angular/core';
import { field, form, FormNodeDirective, type FormNodeValueControl, type FormNodeCheckboxControl } from '@ngblocks/form-nodes';

@Component({
selector: 'app-contract-text',
template: `<input #text [value]="value()" [disabled]="disabled()"
(input)="value.set(text.value)" (blur)="touch.emit()">`,
})
export class ContractTextControl implements FormNodeValueControl<string> {
value = model('');

disabled = input(false);

touch = output<void>();
}

@Component({
selector: 'app-contract-checkbox',
template: `<input #checkbox type="checkbox" [checked]="checked()"
(change)="checked.set(checkbox.checked)" (blur)="touch.emit()">`,
})
export class ContractCheckboxControl implements FormNodeCheckboxControl {
checked = model(false);

touch = output<void>();
}

@Component({
imports: [FormNodeDirective, ContractTextControl, ContractCheckboxControl],
template: `
<app-contract-text [formNode]="form.name" />
<app-contract-checkbox [formNode]="form.subscribe" />
`,
})
export class ProfileComponent {
form = form({
name: field.strict('Ada'),
subscribe: field.strict(false),
});
}

The first generic describes the model value, not the node. Where available, the node generic specializes the optional node signal for direct integration. Ordinary components usually need only the model and selected UI members.

Optional UI state and interaction

FormNodeUiControl groups signal inputs such as disabled, readonly, required, errors, and constraints, plus optional touch, focus(), reset(), and node integration. See its full declaration for exact member types.

Expose touch to report blur or another completed interaction independently of a value change. Use focus() when the binding needs to focus an inner element. A component's reset() hook clears its own transient UI state; the node remains responsible for its form state and value rules.

Input synchronization follows configuration; declaring a compatible input does not override syncInputs or an explicit template binding. See control binding for synchronization precedence and adapter selection.

Binding state and value notifications

A FormNodeBinding describes a rendered connection. It exposes the host, the bound-node signal, error state, and outputs. A FormNodeControl describes the component on the other side of that connection. ControlState is a source-neutral observation facade and does not expose node actions.

formNodeControlValueChange reports the immediate control value; formNodeValueChange follows commit and debounce. Native submission outputs are separate and carry FormNodeSubmitEvent. See the directive reference for event ordering and programmatic-write rules.

CVA components can continue using Angular's ControlValueAccessor contract through the CVA adapter. They do not need to implement these model-based types. See custom controls and advanced integrations for complete adapter workflows.