How to make material <input matInput> become readonly with custom directive? - html

I want to make <input> which using material matInput to become readonly using my custom directive. The directive isControlReadonly will be used to set readonly based on security criteria. The problem is it works on <input> but has no effect on <input matInput>
So.. this first input works, the second doesn't:
<!-- this works -->
<input type="input" formControlName="test_field" isControlReadonly>
<!-- this doesn't works -->
<mat-form-field appearance="standard">
<mat-label>Name</mat-label>
<input matInput type="input" formControlName="customer_name" isControlReadonly>
</mat-form-field>
This is the directive:
import { Directive, ElementRef, OnInit, ViewChildren } from '#angular/core';
import { SecurityService } from './security.service';
#Directive({selector: '[isControlReadonly]'})
export class IsReadonlyDirective implements OnInit {
constructor(
private elementRef: ElementRef,
private securityService: SecurityService
) { }
ngOnInit(): void {
var ro = !this.securityService.user.canEdit
this.elementRef.nativeElement.setAttribute('readonly', ro)
}
Please help how to make that custom directive works with matInput?
Edit Note: I don't want to set readonly from reactive form since directive is much cleaner code. I don't want to add logic to my component, I want the logic in the directive since it is centralized and will be used on all forms.
Thanks

EDIT: provided disable solution before.
To make a input readonly move your logic to "ngAfterViewInit"
Here is a working example for your directive:
#Directive({selector: '[isControlReadonly]'})
export class IsReadonlyDirective implements OnInit, AfterViewInit {
constructor(
private elementRef: ElementRef,
private control: NgControl,
private securityService: SecurityService
) { }
ngAfterViewInit(): void {
var ro = !this.securityService.user.canEdit
this.elementRef.nativeElement.setAttribute('readonly', ro)
}
}

Your directive works fine for all kind of inputs but in case of mat-input, the readonly attribute is set by your directive is overridden by Angular Material's own readonly #Input.
Refer Mat-Input's source code here : Mat-Input Readonly #Input for more info
So what you can do is get your code execution of setting attribute out of the stack and hand it over to event loop. This way your code shall be executed after Angular's manipulations. And the most common way to accomplish that is setTimeout with 0 sec delay
setTimeout(() => {
this.elementRef.nativeElement.setAttribute('readonly', ro)
});
Here is a working BLITZ

Related

check if (child) form is dirty in nested reactive forms

I have a form with different sections (nested formgroups)
How can you check if something changes in a specific section.
HTML:
<div [formGroup]="formGroup">
<div formGroupName="one">
<input type="text" formControlName="email">
<input type="text" formControlName="name">
<div>
</div>
TS:
export class someClass implements OnInit {
formGroup = this.formBuilder.group({
one: this.formBuilder.group({
email: [null, [Validators.required, Validators.pattern('^[a-z0-9._%+-]+#[a-z0-9.-]+\\.[a-z]{2,4}$')]],
name[null],
})
});
get emailControl(): AbstractControl { return this.formGroup.get('one.email'); }
get nameControl(): AbstractControl { return this.formGroup.get('one.name'); }
...
}
for example if I want a class (style) if the form is dirty, I can do something like:
[class.dirty]="formGroup.dirty"
How can I check if the "one" form is dirty?
You can access the group dirtiness by calling
formGroup.get('one').dirty
That returns the FormGroup as AbstractControl, thus with standard control props accessible.
Angular will automatically adds control class, If form is dirty, you can use that class to style as per your need.
div.ng-dirty{
....
}
For More Information

angular : passing the input value to the component

I want to affect the value of this input to a variable 'numToAdd' which is in my component and then add 1 to the variable 'numToAdd', but I can't pass the value of the html input to my component variable 'numToAdd'. How to bind my html input to a variable in the component ?
my html code
<input type="number" id=num class=num>
my component
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-afficher-projets',
templateUrl: './afficher-projets.component.html',
styleUrls: ['./afficher-projets.component.css']
})
export class AfficherProjetsComponent implements OnInit {
ngOnInit(): void {}
numToAdd: number;
constructor() { }
add: void {
this.numToAdd++;
}
}
Two-way binding a primary functionality of angular. Use the ngModel directive to bind input to variables.
<input type="number" id="num" class="num" [(ngModel)]="numToAdd">
The above code binds the variable numToAdd with the input. Find more details here

What is the meaning of the syntax: <element #foo />

I am reading a tutorial and I have found a sample code with a line looking like this:
<input #foo />
I think that it's an input with id="foo". But isn't the correct syntax this one:
<input id="foo" />
It's called a template reference variable in Angular. it can refer to a DOM element,Directive or web component etc. According to the official documentation -
A template reference variable is often a reference to a DOM element within a template. It can also be a reference to an Angular component or directive or a web component.
Source : https://angular.io/guide/template-syntax#ref-vars
Example : We can use #ViewChild() with Template Variable using ElementRef to access Native Element. #ViewChild() can instantiate ElementRef corresponding to a given template reference variable. The template variable name will be passed in #ViewChild() as an argument.
HTML :
<div>
Name: <input type="text" #name> <br/>
City: <input type="text" #city>
</div>
Component code :
import { Component, ViewChild, ElementRef, AfterViewInit } from '#angular/core';
#Component({
selector: 'app-theme',
templateUrl: './apptheme.component.html'
})
export class AppThemeComponent implements AfterViewInit {
#ViewChild('name')
private elName : ElementRef;
#ViewChild('city')
private elCity : ElementRef;
ngAfterViewInit() {
this.elName.nativeElement.style.backgroundColor = 'cyan';
this.elName.nativeElement.style.color = 'red';
this.elCity.nativeElement.style.backgroundColor = 'cyan';
this.elCity.nativeElement.style.color = 'red';
}
}
using #name and #city above we can access the native elements style properties.

Access host component in a structural directive

I'm using Angular 5.2.10. Suppose we have the following template:
<mat-form-field *appEntityValidate>
<input
matInput
type="text"
placeholder="Some input">
</mat-form-field>
In EntityValidateDirective, we need to get an instance of MatFormField it's apllied to.
I've tried a solution suggested here, i.e. simply inject MatFormField:
#Directive({
selector: "[appEntityValidate]"
})
export class EntityValidateDirective {
constructor(
private readonly matFormField: MatFormField
) {
const dfg = 0;
}
}
, but I'm getting an exception from Angular:
No provider for MatFormField!
Here is Stackblitz.
I suspect that this doesn't work because my directive is structural one rather than an attribute one.
So, how to access host component in a structural directive then?
UPDATE: In attempt to address concern raised by #yurzui in comments. Specifically, that MatFormField isn't really a host component in this case, since the above markup gets de-sugarized into something like this:
<ng-template appEntityValidate>
<mat-form-field>
<input matInput type="text" placeholder="Some input">
</mat-form-field>
</ng-template>
Thus, MatFormField becomes a child of the element which EntityValidateDirective is applied to.
To cover this, I've also tried the following:
#Directive({
selector: "[appEntityValidate]"
})
export class EntityValidateDirective implements AfterContentInit, AfterViewInit {
#ContentChild(MatFormField) content;
#ViewChild(MatFormField) view;
public ngAfterViewInit() {
const view = this.view;
}
public ngAfterContentInit() {
const content = this.content;
}
}
But both content and view are undefined in the corresponding lifecycle hook methods.

Angular 5 FormGroup reset doesn't reset validators

I have a form on my page and when I call FormGroup.reset() it sets the forms class to ng-pristine ng-untouched but FormControl.hasError(...) still returns truthy. What am I doing wrong here?
Template
<form [formGroup]="myForm" (ngSubmit)="submitForm(myForm)">
<mat-form-field>
<input matInput formControlName="email" />
<mat-error *ngIf="email.hasError('required')">
Email is a required feild
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput type="password" formControlName="password" />
<mat-error *ngIf="password.hasError('required')">
Password is a required feild
</mat-error>
</mat-form-field>
<button type="submit">Login</button>
</form>
Component
export class MyComponent {
private myForm: FormGroup;
private email: FormControl = new FormContorl('', Validators.required);
private password: FormControl = new FormControl('', Validators.required);
constructor(
private formBuilder: FormBuilder
) {
this.myForm = formBuilder.group({
email: this.email,
password: this.password
});
}
private submitForm(formData: any): void {
this.myForm.reset();
}
}
Plunker
https://embed.plnkr.co/Hlivn4/
It (FormGroup) behaves correctly. Your form requires username and password, thus when you reset the form it should be invalid (i.e. form with no username/password is not valid).
If I understand correctly, your issue here is why the red errors are not there at the first time you load the page (where the form is ALSO invalid) but pop up when you click the button. This issue is particularly prominent when you're using Material.
AFAIK, <mat-error> check the validity of FormGroupDirective, not FormGroup, and resetting FormGroup does not reset FormGroupDirective. It's a bit inconvenient, but to clear <mat-error> you would need to reset FormGroupDirective as well.
To do that, in your template, define a variable as such:
<form [formGroup]="myForm" #formDirective="ngForm"
(ngSubmit)="submitForm(myForm, formDirective)">
And in your component class, call formDirective.resetForm():
private submitForm(formData: any, formDirective: FormGroupDirective): void {
formDirective.resetForm();
this.myForm.reset();
}
GitHub issue: https://github.com/angular/material2/issues/4190
In addition to Harry Ninh's solution, if you'd like to access the formDirective in your component without having to select a form button, then:
Template:
<form
...
#formDirective="ngForm"
>
Component:
import { ViewChild, ... } from '#angular/core';
import { NgForm, ... } from '#angular/forms';
export class MyComponent {
...
#ViewChild('formDirective') private formDirective: NgForm;
constructor(... )
private someFunction(): void {
...
formDirective.resetForm();
}
}
After reading the comments this is the correct approach
// you can put this method in a module and reuse it as needed
resetForm(form: FormGroup) {
form.reset();
Object.keys(form.controls).forEach(key => {
form.get(key).setErrors(null) ;
});
}
There was no need to call form.clearValidators()
Add the property -
#ViewChild(FormGroupDirective) formGroupDirective: FormGroupDirective;
and use this instead of this.myForm.reset();
this.formGroupDirective.resetForm();
This will reset the error display and also do the job of form.reset(). But the form, along with the fields, will still show ng-invalid class
Check this answer for more details - https://stackoverflow.com/a/56518781/9262627
The below solution works for me when trying to reset specific form controller in form group -
this.myForm.get('formCtrlName').reset();
this.myForm.get('formCtrlName').setValidators([Validators.required, Validators.maxLength(45), Validators.minLength(4), Validators.pattern(environment.USER_NAME_REGEX)]);
this.myForm.get('formCtrlName').updateValueAndValidity();
form.reset() won't work on custom form control like Angular Material that's why the function is not working as expected.
My workaround for this is something like this
this.form.reset();
for (let control in this.form.controls) {
this.form.controls[control].setErrors(null);
}
this.form.reset() the issue with this is that it will reset your formcontrol values but not the errors so you need to reset them individually by this line of code
for (let control in this.form.controls) {
this.form.controls[control].setErrors(null);
}
With this you don't need to use FormGroupDirective which is a cleaner solution for me.
Github issue: https://github.com/angular/angular/issues/15741
I found that after calling resetForm() and reset(), submitted was not being reset and remained as true, causing error messages to display. This solution worked for me. I found it while looking for a solution to calling select() and focus() on an input tag, which also wasn't working as expected. Just wrap your lines in a setTimeout(). I think setTimeout is forcing Angular to detect changes, but I could be wrong. It's a bit of a hack, but does the trick.
<form [formGroup]="myFormGroup" #myForm="ngForm">
…
<button mat-raised-button (click)="submitForm()">
</form>
submitForm() {
…
setTimeout(() => {
this.myForm.resetForm();
this.myFormGroup.reset();
}, 0);
}
resetForm() {
this.myFormGroup.reset();
this.myFormGroup.controls.food.setErrors(null);
this.myFormGroup.updateValueAndValidity();
}
UPDATE FROM 2021 - ANGULAR 11.2
The fact to use a [formGroup]="form and a #formDirective="ngForm" directly into the HTML function is not a good practise. Or maybe you would prefer to use #ViewChild, and do it directly from your .ts. Actually, the problem don't come from Angular, but Material.
If you take a look at their GitHub, you will see this :
/** Provider that defines how form controls behave with regards to displaying error messages. */
#Injectable({providedIn: 'root'})
export class ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
return !!(control && control.invalid && (control.touched || (form && form.submitted)));
}
}
The form will keep its submitted state. So you just have to delete the last part of the function.
Here is my solution (tested and working). I have a Material Module, into I've implemented this :
export class ShowOnInvalidTouchedErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl): boolean {
return !!(control && control.invalid && control.touched);
}
}
#NgModule({
providers: [
{
provide: ErrorStateMatcher, useClass: ShowOnInvalidTouchedErrorStateMatcher
}
],
exports: [
MatSnackBarModule,
MatTabsModule,
...
]
});
If you want to use this ErrorStateMatcher on only one form, it's possible. Please see this Material example. This is the same principle.
I had no luck with resetting the form directive. But You can also change the input state to pending to do that as well.
this.myForm.get("email").reset();
this.myForm.get("password").reset();
To anyone whom this may help, I am running Angular 9.1.9 and I didn't want to reset the form/controls just the overall validity of the form so I just ran:
this.registerForm.setErrors(null);
...where registerForm: FormGroup and that reset the form errors, leading to:
this.registerForm.valid
...returning true.
The same can be done for controls:
this.registerForm.get('email').setErrors(null)
As soon as the form is touched, these errors are re-evaluated anyway so if that's not good enough, you may need to have a boolean flag to further pin-down exactly when you want to start showing/hiding error UI.
I did not need to touch the directive in my case.
I was also having the same set of problems. My problem was that i was using mat-form-field and formGroup. After resetting the form submitted flag was not resetting.
So, the solution that worked for me is, putting a directive of ngForm along with formGroup and passing onSubmit(form). Added
#ViewChild('form') form;
in component and then I used
this.form.resetForm();
Nothing from above worked for me (Angular 7.2, Angular Material 7.3.7).
Try to pass with submit method an event on view:
<form [formGroup]="group" (ngSubmit)="onSubmit($event)">
<!-- your form here -->
</form>
Then use it to reset currentTarget and your form afterwards:
public onSubmit(event): void {
// your code here
event.currentTarget.reset()
this.group.reset()
}
Simple fix: use button with type="reset" and function submitForm() together
<form [formGroup]="MyForm" (ngSubmit)="submitForm()">
<input formControlName="Name">
<mat-error>
<span *ngIf="!tunersForm.get('Name').value && tunersForm.get('Name').touched"></span>
</mat-error>
<button type="reset" [disabled]="!MyForm.valid" (click)="submitForm()">Save</button>
</form>