Skip to main content
cngx-src documentation

CngxBindField

DirectivePrimaryv0.1.0WCAG AA

projects/forms/field/bind-field.directive.ts

Import#

import { CngxBindField } from '@cngx/forms/field'

Description#

Universal bridge that connects any control - Material (<mat-select>, <mat-chip-grid>, …), native (<input>, <select>, <textarea>), a custom Signal-Forms FormValueControl<T>, or a legacy Reactive-Forms CVA - to a surrounding <cngx-form-field>.

Place on the same element as the control. The directive derives id, empty, focused, disabled, and errorState purely from the field via the presenter - it never injects the concrete control, so it works uniformly across control types.

Value flow is out of scope here: it runs through the host element's own bindings ([control] for Signal Forms, [formControl] for Reactive Forms). This directive only projects form-field ARIA/state onto the host.

For cngx-native atoms with exotic value semantics (multi-select, compareWith, …), write a specialised bridge (see CngxListboxFieldBridge).

Usage (Signal Forms + mat-select)

<cngx-form-field [field]="f.color">
  <label cngxLabel>Color</label>
  <mat-select cngxBindField [control]="f.color">
    <mat-option value="red">Red</mat-option>
    <mat-option value="green">Green</mat-option>
  </mat-select>
  <cngx-field-errors />
</cngx-form-field>

Usage (Reactive Forms + custom control)

<cngx-form-field [field]="adapted">
  <label cngxLabel>Phone</label>
  <my-phone-input cngxBindField [formControl]="ctrl" />
  <cngx-field-errors />
</cngx-form-field>

https://cngxjs.github.io/cngx/examples/#/forms/field/listbox-forms/material-mat-select-via-cngxbindfield

Metadata#

Host#

Providers#

CNGX_FORM_FIELD_CONTROL
useExisting CngxBindField

Relationships

Index#

Instance Properties#

focused#unknown
Readonly
this.focusedState.asReadonly()

HostBindings#

BindingExpression
[id]id()
[attr.aria-describedby]describedBy()
[attr.aria-labelledby]labelledBy()
[attr.aria-invalid]ariaInvalid()
[attr.aria-required]ariaRequired()
[attr.aria-busy]ariaBusy()
[attr.aria-errormessage]ariaErrorMessage()
[attr.aria-readonly]ariaReadonly()

HostListeners#

EventHandler
(focusin)focusin()
(focusout)focusout()

Material select via cngxBindField

import { ChangeDetectionStrategy, Component, DestroyRef, ViewEncapsulation, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatSelectModule } from '@angular/material/select';

import {
  CngxFormField,
  CngxLabel,
  CngxFieldErrors,
  CngxBindField,
  adaptFormControl,
  CNGX_ERROR_MESSAGES,
} from '@cngx/forms/field';

/**
 * The universal bridge on a third-party control. `[cngxBindField]` projects the
 * `cngx-form-field` ARIA/state onto a bare `mat-select` (a self-contained
 * Material widget - no `mat-form-field` needed). `cngxLabel` owns the label/id;
 * `cngx-field-errors` renders validation.
 *
 * Value flow is NOT cngx's job here - it runs on the control's own
 * `[formControl]` binding. Material couples to Reactive Forms (mat-select is a
 * CVA control), so the field is a `FormControl` lifted into a Signal-Forms-shaped
 * accessor via `adaptFormControl`. The readout shows the bridge-derived signals
 * (`id`/`focused`/`empty`/`errorState`) next to the RF control state.
 */
@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  imports: [ReactiveFormsModule, MatSelectModule, CngxFormField, CngxLabel, CngxFieldErrors, CngxBindField],
  providers: [{ provide: CNGX_ERROR_MESSAGES, useValue: { required: () => 'Please choose a size' } }],
  styleUrl: './material-bind-field.component.scss',
  template: `
    <div class="demo">
      <p class="lead">
        <code>[cngxBindField]</code> instruments a bare <code>mat-select</code>: Material renders
        the trigger, cngx derives the field state. Value via Reactive Forms.
      </p>

      <div class="block">
        <cngx-form-field [field]="sizeField">
          <label cngxLabel>Size</label>
          <mat-select
            cngxBindField
            #bf="cngxBindField"
            [formControl]="sizeControl"
            placeholder="Choose…"
            class="size-select"
          >
            <mat-option value="s">Small</mat-option>
            <mat-option value="m">Medium</mat-option>
            <mat-option value="l">Large</mat-option>
            <mat-option value="xl">X-Large</mat-option>
          </mat-select>
          <cngx-field-errors />
        </cngx-form-field>

        <dl class="state">
          <dt>id</dt><dd>{{ bf.id() }}</dd>
          <dt>focused</dt><dd>{{ bf.focused() }}</dd>
          <dt>empty</dt><dd>{{ bf.empty() }}</dd>
          <dt>errorState</dt><dd>{{ bf.errorState() }}</dd>
          <dt>value</dt><dd>{{ value() || '—' }}</dd>
          <dt>touched</dt><dd>{{ sizeControl.touched }}</dd>
          <dt>valid</dt><dd>{{ sizeControl.valid }}</dd>
        </dl>
      </div>
    </div>
  `,
})
export class MaterialBindFieldExample {
  private readonly destroyRef = inject(DestroyRef);

  protected readonly sizeControl = new FormControl<string>('', {
    validators: [Validators.required],
    nonNullable: true,
  });

  // Lift the Reactive Forms control into a Signal-Forms-shaped accessor so
  // cngx-form-field can consume it via [field].
  protected readonly sizeField = adaptFormControl(this.sizeControl, 'size', this.destroyRef);

  protected readonly value = toSignal(this.sizeControl.valueChanges, {
    initialValue: this.sizeControl.value,
  });
}
export { MaterialBindFieldExample as AppComponent };