Code Maverick
Code Maverick

Reputation: 20415

How to properly configure a reactive select element that uses a nested form group?

I've got an Angular 6, Reactive Forms project that connects to an ASP.NET Web Api 2 / MS SQL Server 2017 back end. I am in the middle of wiring up the front end and have come across a situation I have not yet encountered and can't find examples of how to accomplish:

Nested FormGroups whose object is represented by a select element. What I have so far does not error and it seemingly selects the correct item in the drop down upon load. However, upon selection, it only updates one field and not the whole object.

I think I need to use formGroupName="company" to tell registerOnChange() what control to look for and then some form of formControlName="...", but I don't understand how do so with the select or option elements to achieve what I'm looking for. I need a selection to update the entire object, both the id and name properties.

Click here to view the StackBlitz editor/demo

site-edit.component.html

...
<form class="form-horizontal" novalidate (ngSubmit)="save()" [formGroup]="siteForm">
  <fieldset>
    ...
    <div class="form-group" formGroupName="company">
      <label class="col-md-2 control-label" for="companyId">Company</label>
      <div class="col-md-8">
        <select id="companyId" class="form-control" formControlName="company">
          <option *ngFor="let company of companies" [ngValue]="company.name">
            {{ company.name }}
          </option>
        </select>
      </div>
    </div>
    ...
  </fieldset>
</form>

site-edit.component.ts

siteForm: FormGroup;

// TODO: refactor to use http Company Service instead
companies: Company[] = [
  { id: 1, name: 'Company 1'},
  { id: 2, name: 'Company 2'},
  { id: 3, name: 'Company 3'}
];
site: Site;

constructor(private fb: FormBuilder,
            private siteService: SiteService) { }

ngOnInit(): void {
  this.siteForm = this.fb.group({
    ...
    company: this.fb.group({
      id: null,
      name: ''
    }),
    ...
  });
}

getSite(id: number): void {
  this.siteService.getSite(id)
    .subscribe(site => this.onSiteRetrieved(site));
}

save(): void {
  if (this.siteForm.dirty && this.siteForm.valid) {
    // copy form values over the site object values
    const s = Object.assign({}, this.site, this.siteForm.value);

    // simulate call to save service
    console.log('Saved: ' + JSON.stringify(s));
  }
}

onSiteRetrieved(site: Site): void {
  // clear all the state flags (dirty, touched, etc.)
  if (this.siteForm) {
    this.siteForm.reset();
  }
  this.site = site;

  // update the data on the form
  this.siteForm.setValue({
    ...
    company: {
      id: this.site.company.id,
      name: this.site.company.name
    },
    ...
  });
}  

... more

Upvotes: 1

Views: 1574

Answers (2)

Code Maverick
Code Maverick

Reputation: 20415

I've solved it by following Angular's SetControlValueAccessor Model-Driven Example where it uses object identity to select the option.

Click here to see StackBlitz demo

I removed formGroupName="company" off the wrapper div, used formControlName="company" on the select which is the entire object, and then used [ngValue]="company" on the option which is also the entire object.

See below :

<div class="form-group">
  <label class="col-xs-2 control-label" for="companyId">Company</label>
  <div class="col-xs-8">
    <select id="companyId" 
            class="form-control" 
            formControlName="company"
            [compareWith]="compareCompanies">
      <option *ngFor="let company of companies" [ngValue]="company">
        {{ company.name }}
      </option>
    </select>
  </div>
</div>

The only changes I needed to make in the class were :

  1. Remove the FormGroup assignment of company: this.fb.group({ id: null, name: ''}) and replacing it with company: null.
  2. Implement the compareWith function. Needed because the default comparison uses object identity and different objects have different identities. You can override this method with your own implementation to determine uniqueness.

See below :

ngOnInit(): void {

  this.siteForm = this.fb.group({
    name: ['', Validators.required],
    company: null
  });

  ...

}

compareCompanies(c1: Company, c2: Company): boolean {
  return c1 && c2 ? c1.id === c2.id : c1 === c2;
}

Upvotes: 1

Reza
Reza

Reputation: 19913

since there is a lot of code to wire, I believe this is what you need,

<select formControlName="id">
    <option *ngFor="let company of companies" [ngValue]="company.id">{{country.name}}</option>
  </select>

Update

The above solution is correct, but you need to do some changes in your stackblitz, I fixed it for you please take a look this

  1. [ngValue]="company.id"
  2. formControlName="id"
  3. in your ts code this.siteForm.patchValue instead of setValue

please pay attention company name wont be updated, if you need to change company name as well you have to subscribe to change event and change either your form or change site object and patch form again

Upvotes: 1

Related Questions