2

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)

enter image description here

Second+ Form Trigger (changing the storefront) will default to the first value, even though the form value is correctly set to "US"

enter image description here

What is the correct way to handle this scenario?

4
  • INTERESTING FIND - If I null the countryCodesResponse prior to the API call (this.getCountriesService), it works as intended Commented Apr 22 at 22:43
  • Cannot replicate your issue in this demo Commented Apr 23 at 2:01
  • @YongShun very interesting, going to attempt to create a demo project with the packages I am using (version ^18.0.0) and see if I can replicate it on stackblitz Commented Apr 23 at 2:34
  • Cannot recreate this issue in a new app, even using the same packages... only within this particular app do I have this issue - even as a standalone component. Going to be a bit of a needle in a haystack to try and find this one. Thank you for pointing me in the direction that this is not Angular behavior related, but something to do with the app setup itself. Commented Apr 23 at 3:05

1 Answer 1

0

A quick inspection of the provided code shows that in the subscribtion of your getCountries method you have logic that resets the countryCode form field's value to the first option if the current value of the countryCode field is not available amongst the list of options that are loaded after changing the store front. This is the code

 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);
            }

For example if you select United Kingdom (MX) from Storefront 1 and then switch to Storefront 2 the country options for Storefront 2 are loaded and amongst which United Kingdom (MX) is not available which leads to setting France (FR) for the value of the countryCode field i.e. the countryCode field defaults to the first options. Similar if you select Germany (DE) from Storefront 2 and switch back to Storefront1 the country options for Storefront 1 are loaded in the Storefront select amongst which Germany (DE) is not available which leads to setting the first (default) option for value of the countryCode field which is Spain (CA).

In short you issue has nothing to do with the reactive form control but with the code logic mentioned above. The best solution would be to remove this code

 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);
            }

Stackblitz example here

and reset the countryCode form field value to null each time the store front changes which will force the user to select again a new country without defaulting to the first country option for the select storefront.

onStoreFrontChange(e: any) {
  this.form.controls["countryCode"].reset(null);
}

I would also recommend adding an empty placeholder value in the country select dropdown for better user experience:

     <option [value]="null">
         Select country
     </option>

Alternatively you can reset the countryCode form field value to null only when the selected county is not present in the country select options for the new store front this will persist the selection for countries that are available in both store fronts (like US). If you go with this approach then you don't need the onStoreFronChange handler method, just amend the logic in the subscribtion's next handler to this

  if (this.countryCodesResponse.findIndex((x) => x.countryCode === this.form.controls['countryCode'].value) === -1) {
     console.log('reset if country not found');
     this.form.controls['countryCode'].reset(null);
  }
Sign up to request clarification or add additional context in comments.

2 Comments

Yes correct, however this happens when selecting "US" as the examples show, which is available in all storefronts and is the premise of this issue - which should not change the country since its available in the new list. As Yong Shun pointed out, this issue is not present in a new app build so there is something else happening here outside of the code.
The best option would be to always reset the countries dropdown when the storefront changes no matter if the previously selected country is available amongst the countries for the newly selected storefront. This will lead to the most consistent user experience.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.