Using a FormBuilder, 2 properties are created. One property (storefront
) has a change event trigger which populates data to the select option of the other (countryCode
).
The countryCode
is populated with a default value, which should remain selected until the user changes it, or is a value that is not available in the data source countryCodesResponse
The first time the data is retrieved, the form correctly renders the form value. However on changing the storefront
again, which triggers the change event, the countryCode
form render defaults to the first option in the Select list, even though the form value remains correct.
I believe this issue started happening in Angular 17. Prior to this, this code was operating with the intended behavior.
My thought is this may be due to change detection since the data is updated without the form value being updated, however I am unsure on the correct way to handle this other than setting the countrycode
form value to empty and back again after each data retrieval, or forcing a Change Detection cycle.
Below is an example of the behavior (select a storefront option and then change it - the country should remain as "US")
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { FormGroup, FormsModule, FormBuilder, Validators, FormControl, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'test-page',
template: `
<h1>Test Page</h1>
<p>Testing issue with form data render.</p>
<form [formGroup]="form" role="form">
<div class="container">
<div class="row">
<label class="control-label col-2">Storefront:</label>
<div class="col-10 mb-2" [ngClass]="{'has-error':!form.controls['storefront'].valid && form.controls['storefront'].touched}">
<select class="form-control form-select input-sm" formControlName="storefront">
<option value="1">
Storefront 1
</option>
<option value="2">
Storefront 2
</option>
</select>
</div>
</div>
<div class="row">
<label class="control-label col-2">Country:</label>
<div class="col-10 mb-2" [ngClass]="{'has-error':!form.controls['countryCode'].valid && form.controls['countryCode'].touched}">
<select class="form-control form-select input-sm" formControlName="countryCode">
<option *ngFor="let country of countryCodesResponse" [value]="country.countryCode">
{{country.countryName}} ({{country.countryCode}})
</option>
</select>
</div>
</div>
</div>
</form>
<div>
Form Value: ({{getCountryValue()}})
</div>
`,
styles: [`
`],
standalone: true,
imports: [CommonModule, FormsModule, ReactiveFormsModule],
})
export class TestPageComponent {
public form: FormGroup;
public countryCodesResponse: any[];
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
countryCode: new FormControl(null, [Validators.required]),
storefront: new FormControl(null, [Validators.required])
});
this.form.controls['storefront'].valueChanges.subscribe((value: string) => {
if(!isNaN(Number(value))) {
this.getCountries(Number(value));
}
});
}
// constant ping on chng-det to see current form value
public getCountryValue(): string {
return this.form.controls['countryCode'].value;
}
public getCountries(storefrontId: number) {
this.getCountriesService(storefrontId).subscribe({
next: (response) => {
this.countryCodesResponse = response;
// check if the currently selected country is available in the new list, if not, set the first one as default
if (this.countryCodesResponse.findIndex((x) => x.countryCode === this.form.controls['countryCode'].value) === -1) {
console.log('not found, setting default');
this.form.controls['countryCode'].setValue(this.countryCodesResponse[0].countryCode);
}
},
error: (error) => {
console.error('Error fetching countries:', error);
}
});
}
ngOnInit() {
this.form.controls["countryCode"].setValue('US');
}
// Simulate an API call to fetch country codes for given storefront
public getCountriesService(storefrontId: number) {
switch (storefrontId) {
case 1:
return of([
{countryCode: 'CA', countryName: 'Spain' },
{countryCode: 'MX', countryName: 'United Kingdom' },
{countryCode: 'US', countryName: 'United States' },
]).pipe(delay(500));;
case 2:
return of([
{countryCode: 'FR', countryName: 'France' },
{countryCode: 'DE', countryName: 'Germany' },
{countryCode: 'IT', countryName: 'Italy' },
{countryCode: 'ES', countryName: 'Spain' },
{countryCode: 'GB', countryName: 'United Kingdom' },
{countryCode: 'US', countryName: 'United States' },
]).pipe(delay(500));;
default:
return of([
{countryCode: 'US', countryName: 'United States' }
]).pipe(delay(500));
}
}
}
However what happens is:
First Form Trigger (correctly renders US)
Second+ Form Trigger (changing the storefront) will default to the first value, even though the form value is correctly set to "US"
What is the correct way to handle this scenario?
null
thecountryCodesResponse
prior to the API call (this.getCountriesService
), it works as intended