Behavior of ngModel vs formControlName for radio elements

635 Views Asked by At

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!

VALIDATION_SCENARIO

My question is:

  1. Why is number of instance generated for same element type different for ngModel and formControlName?
  2. What changes are required so that formControlName also returns single instance?

Working Stackbliz version

2

There are 2 best solutions below

0
Aakash Goplani On BEST ANSWER

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:

  1. this.formDirective.addControl(this).
  2. The NgModel's NgControl is added in the Form Directives Tree at the end of this tick. The reason for that can be found here.
  3. The interesting part is that although each 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.

5
Eliseo On

I founded a solution that is to check if yet there're a app-error before adding, see the if (parent.innerHTML.indexOf(componentFactory.selector) < 0) in your dynamic-component.service

  loadComponentIntoNode(
    vcr: ViewContainerRef,
    dynamicItem: DynamicItem,
    parentNode = null
  ): void {
    if (dynamicItem.component) {
      const parent = parentNode || vcr.element.nativeElement;
      const componentFactory =
        this.componentFactoryResolver.resolveComponentFactory(
          dynamicItem.component
        );
      if (parent.innerHTML.indexOf(componentFactory.selector) < 0) {
        const componentRef = vcr.createComponent(componentFactory);
        const newChild =
          componentRef.injector.get(ErrorComponent).elementRef.nativeElement;
        this.renderer.appendChild(parent, newChild);
        (componentRef.instance as DynamicComponent).data = dynamicItem.data;
      }
    }
  }

The problem is, if you check the statusChange that, in a template driven form has as Observers an array of so many elements you has in radio, and an array of one element in model drive forms, But I can not find a solution about this