I'm building a custom component that wraps several form fields and implementing ControlValueAccessor to interact with the reactive form in its parent component. I'm also setting up validation for the custom form component using NG_VALIDATORS, essentially following this guide:
https://blog.thoughtram.io/angular/2016/07/27/custom-form-controls-in-angular-2.html
The child form is then toggled on/off by getting wrapped in a <div> tag with *ngIf.
The problem I'm facing is the validator function is getting "re-hooked up" once for each time the component gets shown and then hidden. Each previous reference to the validator function is remembered, so if the control were to be hidden and then shown again 5 times, the validator gets fired 5 times for each change detection cycle instead of just once.
Any help or guidance is appreciated.
To demonstrate the problem I've added a console.log to the validation function.
plunkr: https://plnkr.co/edit/nIj6WR?p=preview
App Component
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, Component, OnInit } from '@angular/core';
import { ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { ChildFormComponent } from './child-form.component';
@Component({
selector: 'app-root',
template: `
<h2>
Form Value
</h2>
<div>
<pre>
{{form.value | json}}
</pre>
</div>
<form [formGroup]="form">
<label for="value1">Value 1</label>
<input id="value1" type="text" formControlName="value1" />
<h3>Show child form?</h3>
<div>
<label for="showChildFormYes">Yes</label>
<input id="showChildFormYes" type="radio" formControlName="showChildForm" value="yes" />
<label for="showChildFormNo">No</label>
<input id="showChildFormNo" type="radio" formControlName="showChildForm" value="no" />
</div>
<div *ngIf="showChildForm">
<app-child-form formControlName="childValue"></app-child-form>
</div>
</form>
`
})
export class AppComponent implements OnInit {
form: FormGroup;
showChildForm: boolean;
constructor(private formBuilder: FormBuilder) { }
ngOnInit() {
console.log('initializing app form');
this.form = this.formBuilder.group({
value1: '',
childValue: '',
showChildForm: 'no'
});
this.form.get('showChildForm').valueChanges.subscribe(value => {
this.showChildForm = value === 'yes';
});
}
}
@NgModule({
declarations: [
AppComponent,
ChildFormComponent
],
imports: [
BrowserModule,
ReactiveFormsModule,
HttpModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Child Form Component
import {
Component,
OnInit,
OnDestroy,
forwardRef
} from '@angular/core';
import {
FormGroup,
FormControl,
FormBuilder,
ControlValueAccessor,
NG_VALUE_ACCESSOR,
NG_VALIDATORS,
ValidatorFn
} from '@angular/forms';
const CHILD_FORM_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ChildFormComponent),
multi: true
};
const CHILD_FORM_VALIDATORS = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ChildFormComponent),
multi: true
};
function validatorFnFactory() {
return (control: FormControl): ValidatorFn => {
console.log('VALIDATING', control.value);
return null;
}
}
@Component({
selector: 'app-child-form',
template: `
<p>
child-form works!
</p>
<form [formGroup]="form">
<input type="text" formControlName="value1" />
</form>
`,
providers: [
CHILD_FORM_VALUE_ACCESSOR,
CHILD_FORM_VALIDATORS
]
})
export class ChildFormComponent implements OnInit, OnDestroy, ControlValueAccessor {
form: FormGroup;
validator: ValidatorFn;
constructor(private formBuilder: FormBuilder) {
this.validator = validatorFnFactory();
}
ngOnInit() {
console.log('initializing child form');
this.form = this.formBuilder.group({
value1: ''
});
this.form.valueChanges.subscribe(value => {
this.value = value;
})
}
ngOnDestroy() {
console.log('destroying child form');
}
validate(control: FormControl) {
this.validator(control);
}
_value: any = '';
get value(): any { return this._value; };
set value(v: any) {
if (v !== this._value) {
this._value = v;
this.onChange(v);
}
}
writeValue(value: any) {
this._value = value;
this.onChange(value);
}
onChange = (_) => { };
onTouched = () => { };
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}