Populating Multiple Dropdowns in Angular Using forkJoin

The code for this article is available on my GitHub:
JeffreyBane/ALF-My-ForkJoin-App (github.com)

We looked at using concatMap to populate a dropdown prior to assigning a selected index in this article: Avoiding Nested Subscriptions in Angular. Waiting for an observable to complete before starting another observable has many use cases. However today let’s look at a case where we’re going to fire off multiple API calls and we don’t care about the order in which they are run. In fact, we want them to execute in parallel for the best performance.

The use case being – “Go fire off these 3 observables, I don’t care about order, but once they are ALL done, I want to take some other action.”

As always, let’s start a fictitious company. We run a car dealership (mainly because I can’t think of anything else right now). We sell high quality new and used cars for no more than MSRP. In fact, we sell our cars at a discount, treat our customers well, and never lie to them.

Pretend is fun.

For our inventory lookup tool, we use an Angular app that salespeople can use to find out details about the cars on our lot. In our simplified example, our app has dropdowns for things like color, year, and transmission. Our car detail record stores the selected value for each dropdown in the form of the key from a hypothetical dropdown table that stores all our dropdown key and value pairs.

So, to sum up, whenever a car is selected, we have:

  • 3 dropdowns we want to populate
  • We don’t care what order they are populated in
  • However once ALL of the dropdowns populate, we want to take the action of selecting the correct values for each vehicle
  • As well as populating some other form fields

Let’s get started with a service:

>> ng g s vehicle-information

Inside this service, we’re going to simulate a table that stores all our dropdown key / values, classified by type. getDropDownData() will return the values for each select, filtered on the type passed in. We also have another method that returns a vehicle detail record filtered on the vehicleId passed in, getVehicle().

Vehicle Information Service (vehicle-information.service.ts)

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class VehicleInformationService {

  getDropDownData(dropDownType: string): Observable<any[]> {

    const mockJson = [

      { id: 1, type: 'Color', value: 'White' },
      { id: 2, type: 'Color', value: 'Black' },
      { id: 3, type: 'Color', value: 'Silver' },
      { id: 4, type: 'Color', value: 'Blue' },
      { id: 5, type: 'Color', value: 'Red' },
      { id: 6, type: 'Color', value: 'Other' },

      { id: 7, type: 'Year', value: '2022' },
      { id: 8, type: 'Year', value: '2021' },
      { id: 9, type: 'Year', value: '2020' },
      { id: 10, type: 'Year', value: '2019' },
      { id: 11, type: 'Year', value: '2018' },

      { id: 12, type: 'Transmission', value: '5 Speed Manual' },
      { id: 13, type: 'Transmission', value: '6 Speed Manual' },
      { id: 14, type: 'Transmission', value: 'Automatic' }

    ];
    return of(mockJson.filter(item => item.type === dropDownType));
  }

  getVehicle(vehicleId: number): Observable<any> {

    const mockJson = [

      { id: 1, make: 'Ford', model: 'Luxoboat', price: 29999, color: 1, year: 9, transmission: 14 },
      { id: 2, make: 'Bentley', model: 'Excess', price: 249999, color: 4, year: 7, transmission: 14 },
      { id: 3, make: 'BMW', model: 'Service Loaner', price: 69999, color: 2, year: 7, transmission: 13 }

    ];
    return of(mockJson.filter(item => item.id === vehicleId)[0]);
  }
}

Now on to our form. For now, let’s start with doing nothing but populating the 3 dropdowns to demonstrate how forkJoin works. We’ll use some css classes to simulate a “please wait” type message. For now, I’ll just use a ‘light’ them and ‘normal’ theme – ‘light’ means data is loading, with ‘normal’ meaning the data load is complete. Creating a “Please Wait / Data Loading” type of dialog isn’t the purpose of this article but if you want to have a look at a full and complete example of creating a “Data loading” type dialog, have a look at Displaying a Loading Dialog on an Angular Parent Component While Child Components Load.

So back to our form:

App Component Form (app.component.html)

<div [ngClass]="divClass">

  <h2>Welcome to Big Honest Jeff's Big Car Lot of Big Joy</h2>
  <h3>Inventory system</h3>
  
      <div>
          Color: <select formControlName="colorSelect">
              <option *ngFor="let c of colors" [value]="c.id">
                  {{c.value}}
              </option>
          </select>
      </div>
      <div>
          Year: <select formControlName="yearSelect">
              <option *ngFor="let y of years" [value]="y.id">
                  {{y.value}}
              </option>
          </select>
      </div>
      <div>
          Transmission: <select formControlName="transmissionSelect">
              <option *ngFor="let t of transmissions" [value]="t.id">
                  {{t.value}}
              </option>
          </select>
      </div>
  
  </div>

And in the scss file, our 2 simple classes:

App Component Style Sheet (app.component.scss)

.light {
    opacity: .2;

}
.normal {
    opacity: 1;
}

And then finally our component to populate the drop downs. setTimeout() will simulate loading behavior by delaying our form population by 2 seconds:

App Component (app.component.ts)

import { Component } from '@angular/core';
import { forkJoin } from 'rxjs';
import { VehicleInformationService } from './vehicle-information.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  colors: any[];
  years: any[];
  transmissions: any[];
  vehicle: any;
  divClass = 'light';


  constructor(private _vehicleInformationService: VehicleInformationService) {

    this.divClass = 'light';
    forkJoin([
      _vehicleInformationService.getDropDownData('Color'),
      _vehicleInformationService.getDropDownData('Year'),
      _vehicleInformationService.getDropDownData('Transmission')
    ]).subscribe((dds) =>

      setTimeout(() => {

        this.colors = dds[0];
        this.years = dds[1];
        this.transmissions = dds[2];

        this.divClass = 'normal';

      }, 2000)

    );
  }
}

So when we first fire up the app, you’ll notice that the screen has the ‘light’ css class applied while we wait for the 3 dropdowns to be populated:

Once everything is bound to our ngFor variables for our dropdowns, we set the css class to ‘normal’, restoring a complete look:

Fabulous!

Before we add the remainder of our code, let’s have a look at what forkJoin is doing and how it works. Unlike your traditional call to a service method that returns an observable, forkJoin actually takes an array of method calls. This can be 2, 6, or even 43 or more:

    forkJoin([
      _dropDownService.getDropDownData('Color'),
      _dropDownService.getDropDownData('Year'),
      _dropDownService.getDropDownData('Transmission')
    ])

forkJoin then returns, you guessed it, an array of values. Assigning them to variables is as simple as using the array index that corresponds to the order in which they were called:

    ).subscribe((dds) => 
        this.colors = dds[0];
        this.years = dds[1];
        this.transmissions = dds[2];
    )

This is a super useful way to fire off multiple requests when we don’t care about what order they complete in. However, our original use case said we want to do something AFTER these 3 calls complete. In this case, we want to select all the appropriate selected index values for the dropdowns, as well as populate some other form fields.

So let’s do that very thing. We’ll need a higher order operator like concatMap or switchMap to chain our observables together. Since our observables are all just simple simulated API calls (what would typically be an HTTP get request) that return just one value, it’s not super important which operator we use. If any of our observables were to emit more than one value, it would become quite important which operator we use (article forthcoming….) But in this example, we’ll just use concatMap. Because I like concatMap. (To see switchMap really shine check out Implementing a Typeahead search in Angular using switchMap when you have the urge.)

We’ll start in this case by calling our getVehicle() method, storing the retuned value in a variable called ‘vehicle’ and then chaining that to our forkJoin. We’ll hard code an id for this example, but in the real world you’d have a list of vehicles and clicking on a vehicle would bring you to this very detail page and call getVehicle() with the appropriate id. In this case we’ll just use “1”. Once all the observable values are returned in the subscribe, we can assign our selected indexes as well as populate a form field or two. Let’s look at the revised HTML first:

App Component HTML (app.component.html)

<form [formGroup]="myFormGroup">
    <div [ngClass]="divClass">

        <h2>Welcome to Big Honest Jeff's Big Car Lot of Big Joy</h2>
        <h3>Inventory system</h3>

        <div>
            Make: <input type='textbox' formControlName="make">
            Model: <input type='textbox' formControlName="model">
            Price: <input type='textbox' formControlName="price">
        </div>


        <div>
            Color: <select formControlName="colorSelect">
                <option *ngFor="let c of colors" [value]="c.id">
                    {{c.value}}
                </option>
            </select>
        </div>
        <div>
            Year: <select formControlName="yearSelect">
                <option *ngFor="let y of years" [value]="y.id">
                    {{y.value}}
                </option>
            </select>
        </div>
        <div>
            Transmission: <select formControlName="transmissionSelect">
                <option *ngFor="let t of transmissions" [value]="t.id">
                    {{t.value}}
                </option>
            </select>
        </div>


    </div>
</form>

Since we’re using a big boy form now (albeit an absolutely hideous, unstyled form), we need to add an import to our app.module so add ReactiveFormsModule to the imports array:

App Module (app.module.ts)

  imports: [
    BrowserModule,
    AppRoutingModule,
    ReactiveFormsModule
  ],

And finally let’s look at the code of our completed component:

App Component (app.component.ts)

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { concatMap, delay, forkJoin } from 'rxjs';
import { VehicleInformationService } from './vehicle-information.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  colors: any[];
  years: any[];
  transmissions: any[];
  vehicle: any;
  divClass = 'light';

  colorSelect: FormControl;
  yearSelect: FormControl;
  transmissionSelect: FormControl;
  make: FormControl;
  model: FormControl;
  price: FormControl;

  myFormGroup: FormGroup;


  ngOnInit(): void {

    this.colorSelect = new FormControl('');
    this.yearSelect = new FormControl('');
    this.transmissionSelect = new FormControl('');
    this.make = new FormControl('');
    this.model = new FormControl('');
    this.price = new FormControl('');

    this.myFormGroup = new FormGroup({
      colorSelect: this.colorSelect,
      yearSelect: this.yearSelect,
      transmissionSelect: this.transmissionSelect,
      make: this.make,
      model: this.model,
      price: this.price
    })
  }


  constructor(private _vehicleInformationService: VehicleInformationService) {

    this.divClass = 'light';

    _vehicleInformationService.getVehicle(1).pipe(concatMap((data) => {
      this.vehicle = data;
      return forkJoin([
        _vehicleInformationService.getDropDownData('Color'),
        _vehicleInformationService.getDropDownData('Year'),
        _vehicleInformationService.getDropDownData('Transmission')
      ]).pipe(delay(2000))
    }
    )).subscribe((dds) => {
      console.log(this.vehicle);
      this.colors = dds[0];
      this.years = dds[1];
      this.transmissions = dds[2];

      // set form values
      this.colorSelect.setValue(this.vehicle['color']);
      this.yearSelect.setValue(this.vehicle['year']);
      this.transmissionSelect.setValue(this.vehicle['transmission']);
      this.make.setValue(this.vehicle['make']);
      this.model.setValue(this.vehicle['model']);
      this.price.setValue(this.vehicle['price']);

      this.divClass = 'normal';
    });
  }
}

So we’re setting up a proper form in the OnInit then getVehicle(1) begins our chain. (use any id you’d like. As long as it’s 1, 2, or 3.) We then pipe that to concatMap and assign the value that getVehicle() returns to our ‘vehicle’ variable (I’m very creative with naming, you don’t have to tell me). Then instead of setTimout like we used in the previous example, we pipe that to the delay operator. If you’ve never piped to the delay operator, it’s a very useful thing for testing, making it easy to simulate long API or http calls. You can read more about delay in the links at the end of the article. Once we finally subscribe and get our values back from our forkJoin call, we populate our 3 dropdowns then populate everything else on our form using all the values stored in the vehicle object (vehicle 2 in this case):

You can see how forkJoin becomes a very useful tool for returning multiple values when you truly don’t care about what order the observables are returned in, which for many things like dropdown lists, is typically the case.

In summary, I hope you’re getting as excited as I am about all the options you have in rxjs / Angular for piping your results to other operators, piping yet again, and making all kinds of magic happen (I really need a girlfriend).

NOTE: One sort of gotcha when using forkJoin is that if any of the observables in the array generate an error, you’ll lose the values of ALL the observables. In this particular example, I wouldn’t have a problem with that – If some dropdown couldn’t be populated, we don’t want the user to try to edit a form that’s only half populated. I’d like to generate an actual error and prevent any editing of the form data until the situation is corrected.

However, if you DO indeed want, say 2 dropdowns populated if only 2 calls succeed, a way around this (there’s a good example of this method in the learnrxjs.io link at the end of the article) is to simply handle the error on each observable using catch error. So instead of this:

_vehicleInformationService.getDropDownData('Color')

Each of your observables in the forkJoin array would have its own error handler like this:

_vehicleInformationService.getDropDownData('Color').pipe(catchError(error => of(error))),

Now if any call fails, it will simply catch the error and return an error in that array element. As the final step to this method, we now need to test for an error in our assignment code in our subscribe method:


    )).subscribe((dds) => {
      
      if (!(dds[0] instanceof Error)) {
        this.colors = dds[0];
      }

      if (!(dds[1] instanceof Error)) {
        this.years = dds[1];
      }

      if (!(dds[2] instanceof Error)) {
        this.transmissions = dds[2];
      }

The following snippet in our component will demonstrate this:

App Component (app.component.ts)

    this.divClass = 'light';

    _vehicleInformationService.getVehicle(1).pipe(concatMap((data) => {
      this.vehicle = data;
      return forkJoin([
        throwError(() => new Error('Some error occurred'))
.pipe(catchError(error => of(error))),
        _vehicleInformationService.getDropDownData('Year')
.pipe(catchError(error => of(error))),
        _vehicleInformationService.getDropDownData('Transmission')
.pipe(catchError(error => of(error)))
      ]).pipe(delay(2000))
    }
    )).subscribe((dds) => {

      if (!(dds[0] instanceof Error)) {
        this.colors = dds[0];
      }

      if (!(dds[1] instanceof Error)) {
        this.years = dds[1];
      }

      if (!(dds[2] instanceof Error)) {
        this.transmissions = dds[2];
      }

      this.colorSelect.setValue(this.vehicle['color']);
      this.yearSelect.setValue(this.vehicle['year']);
      this.transmissionSelect.setValue(this.vehicle['transmission']);
      this.make.setValue(this.vehicle['make']);
      this.model.setValue(this.vehicle['model']);
      this.price.setValue(this.vehicle['price']);
      this.divClass = 'normal';
    });

Now each item in the forkJoin array is being piped to catchError, which will handle the error if one is raised on each individual call. Also, notice in the first item in the forkJoin array, we’ve replaced the call to get the Color values with a throwError() to generate an error. However, since we’re catching it as well, our subscribe will no longer populate that dropdown if an error is returned:

      if (!(dds[0] instanceof Error)) {
        this.colors = dds[0];
      }

You can see now the dropdowns are populated except for the one that threw an error:

Again, dropdowns are probably not the best use case for this error handling technique, but now you know how to handle preserving observable values from forkJoin should one or more of them happen to fail, should that suit your needs.

You read now:

All things forkJoin: https://www.learnrxjs.io/learn-rxjs/operators/combination/forkjoin


Delay operator: https://www.learnrxjs.io/learn-rxjs/operators/utility/delay