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

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

Related

Error: formGroup expects a FormGroup instance. Please pass one in. Reactive forms Angular

I am using reactive forms in Angular version 10. But getting the following error.
ERROR Error: formGroup expects a FormGroup instance. Please pass one in.
Example:
<div [formGroup]="myGroup">
<input formControlName="firstName">
</div>
In your class:
this.myGroup = new FormGroup({
firstName: new FormControl()
});
at Function.missingFormException (forms.js:1700:1)
at FormGroupDirective._checkFormPresent (forms.js:5632:1)
at FormGroupDirective.ngOnChanges (forms.js:5454:1)
at FormGroupDirective.rememberChangeHistoryAndInvokeOnChangesHook (core.js:2373:1)
at callHook (core.js:3285:1)
at callHooks (core.js:3251:1)
at executeInitAndCheckHooks (core.js:3203:1)
at selectIndexInternal (core.js:6324:1)
at ɵɵadvance (core.js:6306:1)
at PatientInformationComponent_Template (template.html:39:34)
My sample HTML code is as follows.
<div [formGroup]="MyForm">
<input formControlName="firstName">
<input formControlName="lastName">
</div>
My TS code:
export class MyComponent implements OnInit{
MyForm: FormGroup;
constructor( private formbuilder: FormBuilder) {}
ngOnInit() {
this.MyForm= this.formbuilder.group({
firstName: new FormControl("", Validators.maxLength(100)),
lastName: new FormControl("",Validators.maxLength(100)),
});
}
}
Although the form works properly, but the error always shows in the console. I think it might be because of some other lifecycle hook. Could you give me some solution for this.
Since you haven't initialized your form called myForm in .ts code, you should try adding *ngIf and change div HTML tag to form element.
<form *ngIf="form"
[formGroup]="MyForm">
<input formControlName="firstName">
<input formControlName="lastName">
</form>
i had the same issue., for me it was
i declared the form in component.ts
form: FormGroup = this._fb.group({})
and in component.html i used
<form [formGroup]="studentForm" (ngSubmit)="onSubmit()">
i used different names in each place
i had to use same form name in both component.html and component.ts

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>

Angular2 dynamically generating Reactive forms

I have a concept question and would like some advice.
So I have a component, myFormArray, which is a reactive form. It takes in an input array, and creates a number of FormControls accordingly.
#Component({
selector: 'myFormArray',
templateUrl: 'formarray.component.html'
})
export class FormArrayComponent{
#Input() classFields: any[];
userForm = new FormGroup();
ngOnInit(){
// psuedocode here, but I know how to implement
for (# of entries in classFields)
userForm.FormArray.push(new FormControl());
}
Now, in my parent html, I will be dynamically generating multiple myFormArrays. If that is confusing, assume I'm doing this:
<myFormArray [classFields] = "element.subArray"/>
<myFormArray [classFields] = "element2.subArray"/>
<button (click) = "save()"> //I don't know how to implement this!
And at the very end of the page, I want a save button, that can somehow grab all the values the user inputs in to all the forms, and pushes all this data to an array in a Service component. I'm not sure exactly how to do this part. Note that I don't want individual submit buttons for each dynamically generated form component.
How would I implement this functionality? Thanks!
Your start is good, but you have to write your source code differently.
Instead of this example app.components.ts is main component and my-array.component.ts is child component.
Our test data
classFields1: any[] = ['firstname', 'lastname', 'email', 'password'];
classFields2: any[] = ['country', 'city', 'street', 'zipcode'];
Step 1. Use FormBuilder for form creation (app.component.ts)
You must import FormBuilder and FormGroup from #angular/forms like this:
import { FormBuilder, FormGroup } from '#angular/forms';
and then define in constructor:
constructor(private formBuilder: FormBuilder) { }
Step 2. Define new empty FormGrooup
Now you can define new empty FormGroup in ngOnInit like this:
ngOnInit() {
this.myForm = this.formBuilder.group({});
}
Step 3. Create FormControls dynamically (app.component.ts)
Now you can start with dynamically creation of your FormControls by iteration of classFields. For this I would recommend to create own function. This function gets two parameter: arrayName and classFields. With arrayName we can set custom name of our FormArray-control. classFields-Array we will use for iteration. We create constant variable for empty FormArray, which we called arrayControls. After this we iterate over classFields and create for each element FormControl, which we called control, and push this control into arrayControls. At the end of this function we add our arrayControls to our Form with custom name by using arrayName. Here is an example:
createDynamicArrayControls(arrayName: string, classFields: any[]) {
const defaultValue = null;
const arrayControls: FormArray = this.formBuilder.array([]);
classFields.forEach(classField => {
const control = this.formBuilder.control(defaultValue, Validators.required);
arrayControls.push(control);
})
this.myForm.addControl(arrayName, arrayControls);
}
Import FormControl and FormArray from #angular/forms. Your import line should be like this:
import { FormBuilder, FormGroup, FormArray, FormControl } from '#angular/forms';
Now call createDynamicFormControls-Function in ngOnInit.
Step 4. HTML Template for this dynamic Form (app.component.html)
For this example I create following template:
<h1>My Form</h1>
<form [formGroup]="myForm">
<div formGroupName="test1">
<app-my-array [classFields]="classFields1" [arrayFormName]="myForm.controls.test1"></app-my-array>
</div>
<div formGroupName="test2">
<app-my-array [classFields]="classFields2" [arrayFormName]="myForm.controls.test2"></app-my-array>
</div>
<button type="button" (click)="saveForm()">Submit</button>
</form>
Here we have new div element with formGroupName. This group name is our arrayName in our form. We give our form arrays via #Input to my-array.component.
Step 5. MyArrayComponent
Now this component is very simnple:
import { Component, OnInit, Input } from '#angular/core';
import { FormGroup } from '#angular/forms';
#Component({
selector: 'app-my-array',
templateUrl: './my-array.component.html',
styleUrls: ['./my-array.component.css']
})
export class MyArrayComponent implements OnInit {
#Input() classFields: any[];
#Input() arrayFormName: FormGroup;
constructor() { }
ngOnInit() { }
}
Here we have only two #Input varibales. (I know, this variable can have a better names :-) ).
Step 6. HTML for MyArrayComponent
<div [formGroup]="arrayFormName">
<div *ngFor="let class of arrayFormName.controls; let index = index;">
<label [for]="classFields[index]">{{ classFields[index] }}</label>
<input type="text" [id]="classFields[index]" [formControlName]="index" />
</div>
</div>
<br>
And here is working example on Stackblitz: https://stackblitz.com/edit/angular-wawsja
If you have some question ask me in comments or read the Angular documentation abour Reactive Forms here.

check if emails match on blur

I'm trying to check if email field and confirm email field match each other. That is, the user types in their email and then they have to confirm it again. I want the match/validation to happen on blur (when the user presses enter or the textfield loses focus).
Here's my ts file:
import {Component, OnInit} from '#angular/core';
import {User} from './user.interface';
import {FormBuilder, FormGroup, ValidatorFn} from '#angular/forms';
#Component({
selector: 'my-email',
templateUrl: '/app/components/profile/email.component.html',
styleUrls:['styles.css'],
})
export class EmailComponent implements OnInit {
public user : User;
Form : FormGroup;
ngOnInit() {
// initialize model here
this.user = {
Email: '',
confirmEmail: ''
}
if(this.Form.valid) {
this.displayErrors = false;
}
}
constructor(fb: FormBuilder, private cookieService: CookieService, private router: Router) {
this.Form = fb.group({
email: [''],
confirmEmail: ['']
},
{
validator: this.matchingEmailsValidator('email', 'confirmEmail')
});
}
save(model: User, isValid: boolean) {
// call API to save customer
//save email
}
matchingEmailsValidator(emailKey: string, confirmEmailKey: string): ValidatorFn {
return (group: FormGroup): {[key: string]: any} => {
let email = group.controls[emailKey];
let confirmEmail = group.controls[confirmEmailKey];
if (email.value !== confirmEmail.value) {
return {
mismatch: true
};
}
};
}
}
Here's my view:
<form [formGroup]="Form" novalidate (ngSubmit)="Form.valid && save(Form.value, Form.valid)">
<div class="container-fluid">
<div id = "container" class="contain" style="text-align: center">
<div>
<fieldset class="form-group">
<label id = "rounded" class="item item-input .col-md-6 .col-md-offset-3">
<input class="email-address-entry form-control" name="email" type="email" placeholder="name#domain.com" formControlName="email" pattern="^(\\w|[0-9.!#$%&’*+/=?^_\`{|}~-])+#(\\w|[0-9-])+(?:‌​[.](\\w|[0-9-])+)*$"/>
</label>
<p class="Reenter-your-email">Reenter your email to confirm</p>
<label id = "rounded" class="item item-input">
<input class="email-address-entry form-control" (blur)="displayErrors=true" name="confirmEmail" type="email" placeholder="name#domain.com" formControlName="confirmEmail" validateEqual="email"/>
</label>
</fieldset>
</div>
<div>
<label class="entry-invalid">
<p *ngIf="displayErrors && !Form.get('email').valid">The email you entered does not match.</p>
</label>
</div>
<div (click)="Form.get('email').length > 0 ? save(Form.value, Form.valid) : NaN" class="{{ Form.get('email').length > 0 ? 'container-fluid anchorBottomHighlight' : 'container-fluid anchorBottom'}}">
<label class="footerLabel">Confirm</label>
</div>
</div>
</div>
</form>
Currently, with the way it's set up, the validation occurs but it does not get cleared when the correct match is input. I'm wondering how I can setup my view correctly? So the validation message is shown/hidden when the correct match is set and not.
Also it seems like Form.get('email').length > 0 is never greater than 0 / never hit, so my label doesn't toggle to be clickable.
I'm using Angular 2 and reactive forms.
It looks that you're mixing the two form syntaxes: template-driven forms and model-driven forms.
Since you're declaring a form model in your class with FormBuilder, I'm assuming you want a model-driven form.
This means your fields don't need attributes like [(ngModel)] or #EmailAddress.
Instead of that:
<input type="email" [(ngModel)]="user.EmailAddress" required #EmailAddress="ngModel">
Write this:
<!-- Now I'm using `formControlName` to bind the field to the model -->
<!-- Its value must match one of the names you used in the FormBuilder -->
<input type="email" formControlName="email">
ALL of your validators must be declared in the FormBuilder. Not just matchingEmailsValidator, but also required:
this.Form = fb.group({
email: ['', Validators.required],
confirmEmail: ['', Validators.required]
},
{
validator: this.matchingEmailsValidator('email', 'confirmEmail')
});
Now you can access a field with the following syntax:
// In the class
this.Form.get('email').value
this.Form.get('email').errors
<!-- In the template -->
{{ Form.get('email').value }}
{{ Form.get('email').errors }}
You can use these syntaxes to display errors. For example:
<input type="email" formControlName="email">
<p *ngIf="Form.get('email').dirty && Form.get('email').errors.required">
This field is required.
</p>
In the example above, I am displaying an error message if the email field has been touched (i.e. the user tried to enter something) AND the required error is present.
You can also verify that your validation rules are enforced by inspecting the form's markup with your browser's dev tools. Angular should have added classes like .ng-invalid or .ng-valid to the <input> tags that have validation rules.
Finally, regarding your question to check email match on blur. You can't postpone Angular's validation, it will happen in real-time (as the user types). But you could wait for the blur event to display errors.
Combining this last advice with my previous example, here's how you could should an error if the email field is empty AND it has lost focus (blur event):
<input type="email" formControlName="email" (blur)="displayErrors=true">
<p *ngIf="displayErrors && Form.get('email').dirty && Form.get('email').errors.required">
This field is required.
</p>
UPDATE(01-FEB-2017) after Euridice posted this Plunkr:
You still have wayyyyy to much validation code in your template. Like I said, ALL VALIDATORS should be declared IN THE FORM MODEL (with the FormBuilder). More specifically:
The pattern="..." attribute in the email field should be replaced with Validators.pattern() in the form model.
What is the validateEqual="email" attribute in the confirmEmail field? You're not using that anywhere.
The main problem is your test to display the error message: *ngIf="displayErrors && !Form.get('email').valid && Form.get('email').error.mismatch".
First of all, the property is errors with an "s", not error.
Also, your custom validator is setting the error on the form itself, NOT on the email field. This means you should retrieve your mismatch custom error from Form.errors.mismatch, NOT Form.get('email').errors.mismatch.
Here's the updated, working Plunkr: https://plnkr.co/edit/dTjcqlm6rZQxA7E0yZLa?p=preview