Reputation: 2210
I'm using Angular 5.0.0 and Material 5.2.2.
This form has two sub questions in it. The user is submitting two times in one form. This has to stay this way because I present here a very stripped down version of my original form.
After the first submit, in the second subquestion, I validate if there is minimum one checked checkbox. If not I do an this.choicesSecond.setErrors({'incorrect': true});
. This
disables the submit
button in a correct way. But this gives an error:
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'true'. Current value: 'false'.
I think it has to do with with change detection
. If I do an extra change detection with this.changeDetectorRef.detectChanges()
then the error disappears but the submit button is not disabled anymore.
What am I doing wrong?
Template:
<mat-card>
<form *ngIf="myForm" [formGroup]="myForm" (ngSubmit)="onSubmit(myForm.value)" novalidate>
<div *ngIf="!subQuestion">
<mat-card-header>
<mat-card-title>
<h3>Which fruit do you like most?</h3>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-radio-group formControlName="choiceFirst">
<div *ngFor="let fruit of fruits; let i=index" class="space">
<mat-radio-button [value]="fruit">{{fruit}}</mat-radio-button>
</div>
</mat-radio-group>
</mat-card-content>
</div>
<div *ngIf="subQuestion">
<mat-card-header>
<mat-card-title>
<h3>Whichs fruits do you like?</h3>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div *ngFor="let choiceSecond of choicesSecond.controls; let i=index">
<mat-checkbox [formControl]="choiceSecond">{{fruits[i]}}</mat-checkbox>
</div>
</mat-card-content>
</div>
<mat-card-actions>
<button mat-raised-button type="submit" [disabled]="!myForm.valid">Submit</button>
</mat-card-actions>
</form>
</mat-card>
Component:
export class AppComponent {
myForm: FormGroup;
fruits: Array<string> = ["apple", "pear", "kiwi", "banana", "grape", "strawberry", "grapefruit", "melon", "mango", "plum"];
numChecked: number = 0;
subQuestion: boolean = false;
constructor(private formBuilder: FormBuilder, private changeDetectorRef: ChangeDetectorRef) { }
ngOnInit() {
this.myForm = this.formBuilder.group({
'choiceFirst': [null, [Validators.required]],
});
let choicesFormArray = this.fruits.map(fruit => { return this.formBuilder.control(false) });
this.myForm.setControl('choicesSecond', this.formBuilder.array(choicesFormArray));
this.onChangeAnswers();
}
onChangeAnswers() {
this.choicesSecond.valueChanges.subscribe(value => {
let numChecked = value.filter(item => item).length;
if (numChecked === 0 ) this.choicesSecond.setErrors({'incorrect': true});
});
}
get choicesSecond(): FormArray {
return this.myForm.get('choicesSecond') as FormArray;
};
onSubmit(submit) {
if (!this.subQuestion) {
this.subQuestion = true;
let numChecked = this.choicesSecond.controls.filter(item => item.value).length;
if (numChecked === 0 ) this.choicesSecond.setErrors({'incorrect': true});
// this.changeDetectorRef.detectChanges()
}
console.log(submit);
}
}
Upvotes: 1
Views: 3218
Reputation: 2210
The solution @ibenjelloun works well with two adjustments (see also comments under his solution):
if (c.value.length >= min)
needs to be if (c.value.filter(item => item).length >= min)
filtering only the checkboxes that are checked.
the setControl of 'choicesSecond' needs to be done after the first submit in onSubmit
method and not the ngOnInit
hook. Here also you need to do the setValidators
and updateValueAndValidity
.
As @ibenjelloun suggested the solution is better with an extra button going from first subquestion to the second subquestion because implementing a back
button is only possible this way.
Here is the final component that works:
export class AppComponent {
myForm: FormGroup;
fruits: Array<string> = ["apple", "pear", "kiwi", "banana", "grape", "strawberry", "grapefruit", "melon", "mango", "plum"];
subQuestion: boolean = false;
constructor(private formBuilder: FormBuilder) { }
ngOnInit() {
this.myForm = this.formBuilder.group({
'choiceFirst': [null, [Validators.required]],
});
}
minLengthArray(min: number) {
return (c: AbstractControl): { [key: string]: any } => {
if (c.value.filter(item => item).length >= min)
return null;
return { 'minLengthArray': { valid: false } };
}
}
get choicesSecond(): FormArray {
return this.myForm.get('choicesSecond') as FormArray;
};
onSubmit(submit) {
if (!this.subQuestion) {
let choicesSecondFormArray = this.fruits.map(fruit => { return this.formBuilder.control(false) });
this.myForm.setControl('choicesSecond', this.formBuilder.array(choicesSecondFormArray));
this.myForm.get('choicesSecond').setValidators(this.minLengthArray(1));
this.myForm.get('choicesSecond').updateValueAndValidity();
this.subQuestion = true;
}
console.log(submit);
}
}
Upvotes: 0
Reputation: 7733
The issues comes from the this.choicesSecond.setErrors({'incorrect': true});
, when you click submit you create the component and at the same time change its value. This fails in development mode because of the aditional check done by angular. Here is a good article about this error.
For form validation you can use a custom validator, an example in this post :
minLengthArray(min: number) {
return (c: AbstractControl): {[key: string]: any} => {
if (c.value.length >= min)
return null;
return { 'minLengthArray': {valid: false }};
}
}
And for steps, as you are using angular material you could use the mat-stepper.
Upvotes: 3