Reputation: 3824
So I've been doing lots of research but I just can't figure it out.
I want to make a Textbox component using Angular material form controls
Following this tutorial, I've implemented it as follows
textbox.component.html
<mat-form-field>
<input matInput type="text" [placeholder]="placeholder" [(ngModel)]="value" />
<mat-error>This field is required</mat-error>
</mat-form-field>
textbox.component.ts
import { Component, Input, forwardRef, OnDestroy, ElementRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor, FormControl, NgControl } from '@angular/forms';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { MatFormFieldControl } from '@angular/material';
import { Subject } from 'rxjs';
import { FocusMonitor } from '@angular/cdk/a11y';
@Component({
selector: 'text-box',
templateUrl: './text-box.component.html',
styleUrls: ['./text-box.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TextBoxComponent),
multi: true
},
{
provide: MatFormFieldControl,
useExisting: TextBoxComponent
}
]
})
export class TextBoxComponent implements ControlValueAccessor, MatFormFieldControl<any>, OnDestroy {
static nextId = 0;
stateChanges: Subject<void> = new Subject<void>();
id: string = `text-box-${TextBoxComponent.nextId++}`;
ngControl: NgControl = null;
focused: boolean = false;
empty: boolean;
shouldLabelFloat: boolean;
disabled: boolean = false;
errorState: boolean = false;
controlType?: string = 'text-box';
autofilled?: boolean;
describedBy: string = '';
@Input()
get placeholder(): string {
return this._placeholder;
}
set placeholder(value: string) {
this._placeholder = value;
this.stateChanges.next();
}
private _placeholder: string;
@Input()
get required(): boolean {
return this._required;
}
set required(value: boolean) {
this._required = coerceBooleanProperty(value);
this.stateChanges.next();
}
private _required = false;
@Input() name: string;
onChange: any = () => {};
onTouched: any = () => {};
isDisabled: boolean = false;
@Input('value') val: string;
get value(): any {
return this.val;
}
set value(val: any) {
this.val = val;
this.errorState = !val;
this.onChange(val);
this.onTouched();
this.stateChanges.next();
}
constructor(private fm: FocusMonitor, private elRef: ElementRef<HTMLElement>) {
fm.monitor(elRef, true).subscribe(origin => {
this.focused = !!origin;
this.stateChanges.next();
});
}
writeValue(value: any): void {
if (value) {
this.value = value;
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.isDisabled = isDisabled;
}
setDescribedByIds(ids: string[]): void {
this.describedBy = ids.join(' ');
}
onContainerClick(event: MouseEvent): void {
if ((event.target as Element).tagName.toLowerCase() != 'input') {
this.elRef.nativeElement.querySelector('input')!.focus();
}
}
ngOnDestroy(): void {
this.stateChanges.complete();
this.fm.stopMonitoring(this.elRef);
}
}
and Basically use it in a form like that:
<form [formGroup]="someForm" (ngSubmit)="onSubmit()">
<acs-text-box formControlName="username" [placeholder]="'Form control'"> </acs-text-box>
<my-button [type]="'submit'">Submit</my-button>
</form>
So here is my question, I'm trying to make render inside the textbox.component.html template but it doesn't work
I've tried several things like setting errorState = true in textbox.component.ts but nothing happens.
I've tried this
<mat-form-field>
<acs-text-box formControlName="username" [placeholder]="'Form control'"> </acs-text-box>
<mat-error>This field is required</mat-error>
</mat-form-field>
It works fine with setting errorState = true, but When I take mat-error back inside of the textbox component template it doesn't work.
Is there a way to make render inside the textbox component template? or is it just not doable with angular material and i shall implement it my own way?
Thanks in advance!!
Upvotes: 6
Views: 7802
Reputation: 1880
The accepeted answer didn't help me.
I think the problem arises when the form control exposed by the custom component, is not the same form control used by the internal material input, therefor when the material form control is not invalid, the errors won't show (see docs quote below).
It's a bit hacky but you could pass the ngControl (exposed form control) to the ErrorStateMatcher of the internal material input.
ErrorStateMatcher explanation in official docs:
"By default, these error messages are shown when the control is invalid and either the user has interacted with (touched) the element or the parent form has been submitted. If you wish to override this behavior (e.g. to show the error as soon as the invalid control is dirty or when a parent form group is invalid), you can use the errorStateMatcher property of the matInput. The property takes an instance of an ErrorStateMatcher object. An ErrorStateMatcher must implement a single method isErrorState which takes the FormControl for this matInput as well as the parent form and returns a boolean indicating whether errors should be shown."
Code examples can be found in the docs link.
Good luck...
Upvotes: 0
Reputation: 2078
Try to do like this :
<mat-form-field>
<input matInput type="text" [placeholder]="placeholder" [(ngModel)]="value" required #inputValue="ngModel" />
<mat-error *ngIf="(inputValue.touched && inputValue.invalid)">This field is required</mat-error>
</mat-form-field>
Upvotes: 4