AI Skill Report Card
Creating Angular Material Form Controls
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.
Quick Start15 / 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' })
Workflow15 / 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
TypeScriptexport 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
TypeScriptget 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
TypeScriptonFocusIn() { 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
TypeScriptsetDescribedByIds(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
Examples17 / 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
Best Practices
- Always emit on
stateChangeswhen any property affecting the form field changes - Use
@HostBinding('class.floating')forshouldLabelFloatto trigger CSS transitions - Include
role="group"for multi-input components - Link to parent form field label with
aria-labelledby - Handle both
focusinandfocusoutevents properly - Complete
stateChangessubject inngOnDestroy - Use static
nextIdfor unique IDs across component instances
Common Pitfalls
- 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