AI Skill Report Card
Creating Angular Material Form Controls
Quick Start15 / 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
Workflow15 / 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
- Basic Structure
TypeScript@Component({ providers: [{provide: MatFormFieldControl, useExisting: YourComponent}] }) export class YourComponent implements MatFormFieldControl<DataType> {
- 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;
- 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; }
- 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; }
- Methods
TypeScriptsetDescribedByIds(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
Examples18 / 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
TypeScriptconstructor( 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
Best Practices
- 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()
Common Pitfalls
- 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