While working with validating same radio input elements with two different approaches viz. template-driven-form and model-driven-form, I am stuck with the scenario where for template-driven-form, using ngModel
, I get single instance of control for 3 radio elements but for model-driven-form, using formControlName
, I get 3 separate instances.
<!-- template-driven-form.component.html -->
<div class="form-group gender">
<label for="gender">Select Gender:</label>
<div class="radio" *ngFor="let gender of genders">
<input type="radio" name="gender" [value]="gender" ngModel appFormControlValidation validationMsgId="gender" required />
<label>{{ gender }}</label>
</div>
</div>
<!-- model-driven-form.component.html -->
<div class="form-group gender">
<label for="gender">Select Gender:</label>
<div class="radio" *ngFor="let gender of genders">
<input type="radio" name="gender" [value]="gender" formControlName="gender" appFormControlValidation validationMsgId="gender" required />
<label>{{ gender }}</label>
</div>
</div>
// model-driven-form.component.ts
genders: string[] = ['Male', 'Female', 'Other'];
this.modelForm = new FormGroup({
gender: new FormControl(null, [Validators.required])
});
// template-driven-form.component.ts
genders: string[] = ['Male', 'Female', 'Other'];
// form-control-directive
(this.control as NgControl).statusChanges.subscribe(
// returns single instance for 3 radio elements -> template form
// returns 3 instance for 3 radio elements -> model form
);
As from snippet above, I am using same HTML structure for both forms yet number instances vary. Problem here is when the validation happens, for template-driven-form, I get error message only once (which is expected scenario) but for model-driven-form, I get error messages displayed 3 times!
My question is:
ngModel
and formControlName
?formControlName
also returns single instance?Working Stackbliz version
Answered by @AndreiGatej - https://github.com/indepth-dev/community/discussions/53
Description:
After a quick investigation, it is actually expected to get three instances. Concretely, the statusChanges
from this.statusChangeSubscription = this.control?.statusChanges?.subscribe()
is subscribed 3 times. If you have N
radio buttons, then you'll end up with N
instances of that directive. This implies that although you're having N
instances, they will all inject the same NgControl
instance(which can be NgModel
, FormControlName
and FormControlDirective
). And that NgControl
instance is, in the case of reactive forms, this one:
gender: new FormControl(null, [Validators.required]),
The reason this doesn't apparently happen when using NgModel
is that when using Template-Driven forms, the form control directives are created on the fly. This is the flow that sets up an NgModel
directive:
this.formDirective.addControl(this)
.NgModel
's NgControl
is added in the Form Directives Tree at the end of this tick. The reason for that can be found here.NgModel
directive inherently creates a unique FormControl
instance, in the directives tree there will be only one FormControl
instance which will be shared by all the NgModel
which share the same name. Here is the piece of logic which indicates that a single FormControl
instance will be shared:if (this.controls[name]) return this.controls[name];
this.controls[name] = control;
control.setParent(this);
control._registerOnCollectionChange(this._onCollectionChange);
return control;
registerControl
is called after the resolvedPromise
promise has been resolved. It's also there where dir.control
is assigned to whatever registerControl
returns.
So, the directive's ngOnInit
is called before that promise(which is essentially Promise.resolve()
) resolves. At the end of the current tick, all the NgModel directives will share the same FormControl
instance. This is why it works as expected.