AI Skill Report Card

Creating Angular Material Form Controls

A85·Apr 20, 2026·Source: Extension-page
15 / 15
TypeScript
@Component({ selector: 'custom-input', template: `<input [value]="value" (input)="onInput($event)">`, providers: [{provide: MatFormFieldControl, useExisting: CustomInput}] }) export class CustomInput implements MatFormFieldControl<string> { stateChanges = new Subject<void>(); static nextId = 0; @HostBinding() id = `custom-input-${CustomInput.nextId++}`; value = ''; placeholder = ''; focused = false; required = false; disabled = false; errorState = false; controlType = 'custom-input'; get empty() { return !this.value; } get shouldLabelFloat() { return this.focused || !this.empty; } ngControl: NgControl = null; onInput(event: Event) { this.value = (event.target as HTMLInputElement).value; this.stateChanges.next(); } setDescribedByIds(ids: string[]) {} onContainerClick() {} ngOnDestroy() { this.stateChanges.complete(); } }
Recommendation
Add input/output pairs showing complete mat-form-field usage with the custom controls
15 / 15

Progress:

  • Create component implementing MatFormFieldControl<T>
  • Add provider for MatFormFieldControl
  • Implement required properties: value, stateChanges, id
  • Implement UI state properties: placeholder, focused, empty, shouldLabelFloat
  • Implement form properties: required, disabled, errorState, ngControl
  • Add control metadata: controlType
  • Implement accessibility methods: setDescribedByIds, onContainerClick
  • Handle lifecycle: ngOnDestroy
  • Add ControlValueAccessor if needed

Implementation Steps

  1. Basic Structure
TypeScript
@Component({ providers: [{provide: MatFormFieldControl, useExisting: YourComponent}] }) export class YourComponent implements MatFormFieldControl<DataType> {
  1. Core Properties
TypeScript
// Required stateChanges = new Subject<void>(); static nextId = 0; @HostBinding() id = `your-control-${YourComponent.nextId++}`; controlType = 'your-control-type'; // Value management @Input() get value(): DataType | null { return this._value; } set value(val: DataType | null) { this._value = val; this.stateChanges.next(); } private _value: DataType | null = null;
  1. State Properties
TypeScript
@Input() get placeholder() { return this._placeholder; } set placeholder(plh: string) { this._placeholder = plh; this.stateChanges.next(); } private _placeholder = ''; focused = false; get empty(): boolean { /* your logic */ } get shouldLabelFloat(): boolean { return this.focused || !this.empty; }
  1. Form Integration
TypeScript
@Input() get required(): boolean { return this._required; } set required(req: BooleanInput) { this._required = coerceBooleanProperty(req); this.stateChanges.next(); } private _required = false; @Input() get disabled(): boolean { return this._disabled; } set disabled(value: BooleanInput) { this._disabled = coerceBooleanProperty(value); this.stateChanges.next(); } private _disabled = false; get errorState(): boolean { return this.ngControl?.invalid && this.touched; }
  1. Methods
TypeScript
setDescribedByIds(ids: string[]) { const element = this.elementRef.nativeElement.querySelector('input'); element?.setAttribute('aria-describedby', ids.join(' ')); } onContainerClick(event: MouseEvent) { if ((event.target as Element).tagName !== 'INPUT') { this.elementRef.nativeElement.querySelector('input')?.focus(); } }
Recommendation
Include template examples showing how to use these controls in actual forms
18 / 20

Example 1: Phone Number Input

TypeScript
@Component({ selector: 'phone-input', template: ` <div role="group" [formGroup]="parts"> <input formControlName="area" maxlength="3"> <span>-</span> <input formControlName="exchange" maxlength="3"> <span>-</span> <input formControlName="subscriber" maxlength="4"> </div> `, providers: [{provide: MatFormFieldControl, useExisting: PhoneInput}] }) export class PhoneInput implements MatFormFieldControl<PhoneNumber> { parts = this.fb.group({ area: '', exchange: '', subscriber: '' }); get value(): PhoneNumber | null { const {area, exchange, subscriber} = this.parts.value; return area.length === 3 && exchange.length === 3 && subscriber.length === 4 ? new PhoneNumber(area, exchange, subscriber) : null; } get empty(): boolean { const {area, exchange, subscriber} = this.parts.value; return !area && !exchange && !subscriber; } }

Example 2: With ControlValueAccessor

TypeScript
constructor( private fb: FormBuilder, @Optional() @Self() public ngControl: NgControl ) { if (this.ngControl) { this.ngControl.valueAccessor = this; } } // ControlValueAccessor methods writeValue(value: DataType): void { this.value = value; } registerOnChange(fn: (value: DataType) => void): void { this.onChange = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; }
Recommendation
Condense the workflow section by removing some redundant code examples that repeat the quick start
  • Use coerceBooleanProperty for boolean inputs to handle string attributes
  • Emit stateChanges whenever any property that affects the form field changes
  • Generate unique IDs using static counter for accessibility
  • Handle focus properly - update focused state and call onTouched when appropriate
  • Implement proper empty logic - determines when placeholder floats
  • Add role="group" for multi-input controls with aria-labelledby
  • Link to parent form field label for accessibility using parentFormField?.getLabelId()
  • Forgetting to emit stateChanges - form field won't update properly
  • Not completing stateChanges in ngOnDestroy - memory leaks
  • Circular dependency with ControlValueAccessor - remove NG_VALUE_ACCESSOR provider, set valueAccessor directly
  • Not handling aria-describedby - poor screen reader experience
  • Incorrect shouldLabelFloat logic - placeholder behavior will be wrong
  • Missing @HostBinding() on id - accessibility attributes won't work
  • Not checking relatedTarget in focusout - focus state bugs with child elements
  • Hardcoded controlType - breaks form field styling system
0
Grade AAI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
15/15
Workflow
15/15
Examples
18/20
Completeness
20/20
Format
15/15
Conciseness
13/15