Creating contextual drop downs using RxJs, Angular 7 and Angular Material

Let's start this post off with a scenario:

[
  {
    type: "Bird",
    options: [
      {species: "Hawk"},
      {species: "Eagle"},
      {species: "Crow"}
      ]
 
  },
  {
    type: "Reptile",
    options: [
      {species: "Snake"},
      {species: "Crocodile"},
      {species: "Gecko"}
      ]
  },
  {
    type: "Mammal",
    options: [
      {species: "Human"},
      {species: "Monkey"},
      {species: "Ape"}
      ]
  },
  {
      type: "Fish",
      options: [
        {species: "Shark"},
        {species: "Cod"},
        {species: "Trout"}
        ]
    }
]

Above we have a snippet of JSON (excuse the formatting), pretty simple, let's say we have a need to create a drop down for each object with the value showing being the type in each object.

Easy of course, now let's say we need to show the options array as a second drop down depending on which type is selected in the previous drop down, ok so a bit of logic required, not too hard though still.

Ok lets up it a tad more and say that both of these drop downs must allow multiple selections!

It's not an uncommon requirement, and by now I expect many a reader will have an idea on how to implement it, but I really think this reactive approach offers a straightforward and readable solution.

First off I have used Angular Material for some styling and to handle a few nice to have styles, but with a bit of tweaking, there is no reason this solution should not work without it.

Let's start off with the typescript:

private formGroup = new FormGroup({
  genus: new FormControl([]),
  subgenus: new FormControl([])
})



Place the above in your component, you will also need to add the following into your modules import array:

ReactiveFormsModule

All we are doing above is setting the backings for each of our drop downs, reactive forms are incredibly useful.
For me though, one of their big draws is the fact you can use the valueChanges property on both a form group and a form control to get an observable.
This observable emits the selected values within the form control or group

Next, let's add to our template:


<form 
[formGroup]="formGroup">
    <mat-form-field>
        <mat-select 
placeholder="Select a genus" formControlName="genus" multiple>
            <mat-option>Value</mat-option>
        </mat-select>
    </mat-form-field>
<br/>
    <mat-form-field>
        <mat-select 
placeholder="Select a subgenus" formControlName="subgenus"             multiple>
            <mat-option>
                Value
                        </mat-option>
        </mat-select>
    </mat-form-field>
</form>


A straightforward template, of course, we will need to add to it but not by much, for now, we have a template which binds to our form group and controls, we also specify the selects are multiple selects.

At this point, if you are using Angular Material, then I suggest running your app to ensure your themes are working and maybe add some more dummy options to try it out.

Next comes linking up some observables, remember I showed JSON earlier? Well, source$ will be the observable that holds that JSON, I am creating an Observable from the array, but more than likely you will have an observable from using Angulars HttpClient.

private source$ = of([
  {type: "Bird", options: [{species: "Hawk"}, {species: "Eagle"}, {species: "Crow"}]},
  {type: "Reptile", options: [{species: "Snake"}, {species: "Crocodile"}, {species: "Gecko"}]},
  {type: "Mammal", options: [{species: "Human"}, {species: "Monkey"}, {species: "Ape"}]},
  {type: "Fish", options: [{species: "Shark"}, {species: "Cod"}, {species: "Trout"}]}
]);

Nothing much to talk about, regarding the above code, note the changes we now make to the template below:

<form [formGroup]="formGroup">
    <mat-form-field>
        <mat-select placeholder="Select a genus" formControlName="genus"                              multiple>
            <mat-option *ngFor="let species of source$ | async"                                          [value]="species">{{species.type}}</mat-option>
        </mat-select>
    </mat-form-field>
<br/>
...

There are a few changes we should talk about here:

The use of the async pipe in the ngFor is essential when working with observables, it does a lot of heavy lifting for us. Next is the addition of [value] this tells angular what the value will be when something is selected, in our case it is species, which turns out to be an object.
Finally, we have added something to display which is the species type.

By now you should have a working drop down multiple select, this is a great starting point but now comes the 'hard' part which is getting the second drop down to populate based on the selections of the first.

This is achieved by adding this to the typescript:

private subSource$ = this.formGroup.get('genus').valueChanges.pipe(
  map((val: any[]) => val.reduce((pre, elem) => [...pre, elem.options], [])),
  map(val => val.flat()));


There is a lot going on in this small piece of code, we will make it more expressive later on!

For now, let's understand the logic and how it works.

First of all, we create a new observable called subSource$, we do this by using valueChanges property on the genus form control which we made earlier, we get that control via the formGroup.

Now comes the interesting part, the pipe, we use the first map operator to reduce our array of selected objects into an array of arrays, these arrays are the option arrays for each species that was selected.

The second map operator is used to flatten this array of arrays into an array of objects containing the species property (yes it could have been a stream but in a real-world scenario its likely to have been an object).

subSource$ observable will now emit an array of objects each time we change our selections in the first dropdown.

All that is left to do now is to make some changes to our template, here is the final template with the changes boldened:

<form [formGroup]="formGroup">
    <mat-form-field>
        <mat-select placeholder="Select a genus" formControlName="genus" multiple>
            <mat-option *ngFor="let species of source$ | async" [value]="species">{{species.type}}</mat-option>
        </mat-select>
    </mat-form-field>
<br/>
    <mat-form-field>
        <mat-select placeholder="Select a subgenus" formControlName="subgenus" multiple>
            <mat-option *ngFor="let subspecies of subSource$ | async" [value]="subspecies">                             {{subspecies.species}}
             </mat-option>
        </mat-select>
    </mat-form-field>
</form>

(once again sorry for the formatting)

By now this should not need any more explaining as it is a rehash of the previous dropdown but powered by our second observable.

Now we have two multi dropdowns, the second dropdown populates itself based on the choices in the first.

More importantly, the solution is fairly simple, very short and expressive...I did, however, say I would clean up that pipe, so here is the full version with some cleaning up in bold:

private formGroup = new FormGroup({
  genus: new FormControl([]),
  subgenus: new FormControl([])
})

private source$ = of([
  {type: "Bird", options: [{species: "Hawk"}, {species: "Eagle"}, {species: "Crow"}]},
  {type: "Reptile", options: [{species: "Snake"}, {species: "Crocodile"}, {species: "Gecko"}]},
  {type: "Mammal", options: [{species: "Human"}, {species: "Monkey"}, {species: "Ape"}]},
  {type: "Fish", options: [{species: "Shark"}, {species: "Cod"}, {species: "Trout"}]}
]);

private fromArrayObjectsToArrayOfOptions = (val : any[]) => (val.reduce((pre, elem) => [...pre, elem.options], []));
private fromArrayOfArraysToArray = (val) => (val.flat());

private subSource$ = this.formGroup.get('genus').valueChanges.pipe(
  map(this.fromArrayObjectsToArrayOfOptions),
  map(this.fromArrayOfArraysToArray));


Now we have pure functions that can be easily tested and reused wherever you please, despite my lack of good naming for these functions we have an expressive pipe (which would be more so if those names were better and my objects were typed appropriately, I have not done this because your types and namings will differ for each scenario and I am lazy :( ).

The result is something like this:




There we have it a drop-down showing us selections through the context of another dropdown, done using a functional, reactive approach.

I like how the solution contains around 9 lines of expressive, declarative code in our component, the markup is simple enough and the solution is highly testable and the functions reusable, what's more is there is no reason you can't follow this to the next level and have form controls based on the second drop down and so on so forth.

This really is the proverbial tip of the iceberg though, there are many more scenarios we can resolve around forms and contextual views really simply using Angular reactive forms and the power of RxJs and its Observable.

I hope you will continue to follow me to see just how deep this iceberg goes!


Comments

  1. Hello sir, Thanks for this tutorial. But I need your some time for explanation of following:
    private fromArrayObjectsToArrayOfOptions = (val : any[]) => (val.reduce((pre, elem) => [...pre, elem.options], []));
    So please provide me any youtube resource so that I and learn and able to understand this type of concept. I need much focus on reduce function and ...pre, elem.option.
    Please accept my request and provide me your bless.

    ReplyDelete

Post a Comment

Popular posts from this blog

Angular material table with angular 7 - Defining columns using ngFor

From nothing to an Angular 7 app running in 5 minutes with AWS