AI Skill Report Card

Creating Angular Material Form Controls

A-85·Apr 20, 2026·Source: Extension-page

Creating Angular Material Form Controls

Creates custom components that work seamlessly with <mat-form-field>, providing Material Design styling, floating labels, hints, and error states.

15 / 15
TypeScript
@Component({ selector: 'custom-tel-input', template: ` <div role="group" [formGroup]="parts" [attr.aria-describedby]="describedBy" [attr.aria-labelledby]="parentFormField?.getLabelId()"> <input formControlName="area" maxlength="3" (focusin)="onFocusIn()" (focusout)="onFocusOut($event)"> <span>–</span> <input formControlName="exchange" maxlength="3" (focusin)="onFocusIn()" (focusout)="onFocusOut($event)"> <span>–</span> <input formControlName="subscriber" maxlength="4" (focusin)="onFocusIn()" (focusout)="onFocusOut($event)"> </div> `, providers: [{provide: MatFormFieldControl, useExisting: CustomTelInput}] }) export class CustomTelInput implements MatFormFieldControl<string>, OnDestroy { static nextId = 0; stateChanges = new Subject<void>(); focused = false; touched = false; controlType = 'custom-tel-input'; errorState = false; @HostBinding() id = `custom-tel-input-${CustomTelInput.nextId++}`; @HostBinding('class.floating') get shouldLabelFloat() { return this.focused || !this.empty; } parts: FormGroup; constructor( fb: FormBuilder, private _elementRef: ElementRef, @Optional() public parentFormField: MatFormField, @Optional() @Self() public ngControl: NgControl ) { this.parts = fb.group({ area: '', exchange: '', subscriber: '' }); } }
Recommendation
Add concrete input/output pairs showing the actual phone number transformation (e.g., '5551234567' → { area: '555', exchange: '123', subscriber: '4567' })
15 / 15

Implementation Checklist:

  • Implement MatFormFieldControl interface
  • Add provider for MatFormFieldControl
  • Implement required properties and methods
  • Handle focus and blur events
  • Set up accessibility attributes
  • Test with mat-form-field features

Step 1: Interface Implementation

TypeScript
export class CustomInput implements MatFormFieldControl<YourDataType> { // Required properties stateChanges = new Subject<void>(); focused = false; controlType = 'your-control-type'; errorState = false; @HostBinding() id = `your-control-${CustomInput.nextId++}`; static nextId = 0; }

Step 2: Value Management

TypeScript
@Input() get value(): YourDataType | null { // Return current value or null if invalid } set value(val: YourDataType | null) { // Set internal value and emit state change this.stateChanges.next(); }

Step 3: State Properties

TypeScript
get empty(): boolean { return !this.value; // Your empty logic } @HostBinding('class.floating') get shouldLabelFloat(): boolean { return this.focused || !this.empty; } @Input() get placeholder(): string { return this._placeholder; } set placeholder(value: string) { this._placeholder = value; this.stateChanges.next(); } @Input() get required(): boolean { return this._required; } set required(value: BooleanInput) { this._required = coerceBooleanProperty(value); this.stateChanges.next(); } @Input() get disabled(): boolean { return this._disabled; } set disabled(value: BooleanInput) { this._disabled = coerceBooleanProperty(value); this.stateChanges.next(); }

Step 4: Focus Management

TypeScript
onFocusIn() { if (!this.focused) { this.focused = true; this.stateChanges.next(); } } onFocusOut(event: FocusEvent) { if (!this._elementRef.nativeElement.contains(event.relatedTarget as Element)) { this.touched = true; this.focused = false; this.stateChanges.next(); } }

Step 5: Required Methods

TypeScript
setDescribedByIds(ids: string[]) { const controlElement = this._elementRef.nativeElement.querySelector('.your-control-container'); controlElement?.setAttribute('aria-describedby', ids.join(' ')); } onContainerClick(event: MouseEvent) { if ((event.target as Element).tagName.toLowerCase() !== 'input') { this._elementRef.nativeElement.querySelector('input')?.focus(); } } ngOnDestroy() { this.stateChanges.complete(); }
Recommendation
Include a complete working example with the data type definition (MyTel class) referenced in the examples
17 / 20

Example 1: Phone Number Input

TypeScript
// Input: User types "555", "123", "4567" // Output: MyTel { area: "555", exchange: "123", subscriber: "4567" } get value(): MyTel | null { const n = this.parts.value; if (n.area.length === 3 && n.exchange.length === 3 && n.subscriber.length === 4) { return new MyTel(n.area, n.exchange, n.subscriber); } return null; }

Example 2: Usage in Template

HTML
<mat-form-field> <custom-tel-input placeholder="Phone number" required></custom-tel-input> <mat-icon matPrefix>phone</mat-icon> <mat-hint>Include area code</mat-hint> </mat-form-field>
Recommendation
Consider condensing the Step-by-step workflow slightly - some sections could be merged or simplified
  • Always emit on stateChanges when any property affecting the form field changes
  • Use @HostBinding('class.floating') for shouldLabelFloat to trigger CSS transitions
  • Include role="group" for multi-input components
  • Link to parent form field label with aria-labelledby
  • Handle both focusin and focusout events properly
  • Complete stateChanges subject in ngOnDestroy
  • Use static nextId for unique IDs across component instances
  • Forgetting stateChanges emissions - Form field won't update without them
  • Not handling relatedTarget in focusout - Focus state will be incorrect when moving between internal inputs
  • Missing aria-describedby updates - Screen readers won't announce errors/hints
  • Circular dependency with NG_VALUE_ACCESSOR - Set valueAccessor directly in constructor instead
  • Not implementing shouldLabelFloat - Placeholder will overlap your control
  • Ignoring controlType - Prevents custom styling based on control type
0
Grade A-AI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
15/15
Workflow
15/15
Examples
17/20
Completeness
20/20
Format
15/15
Conciseness
13/15