Implementing a Typeahead search in Angular using switchMap

The code for this Angular Typeahead is on my GitHub:
JeffreyBane/ALF-My-TypeAhead-App (github.com)

I know what you’re thinking. “I want to have a sexy looking web form with a sweet Angular Typeahead component so all the women and or men will want me.” So, spoiler – nobody is going to be more attracted to you because you’ve implemented a Typeahead search. BUT it will give your users a much more satisfying usability experience in your app.

That’s kind of the same! (That’s not the same at all.)

So, let’s continue our never-ending quest to understand all the higher order mapping operators by coding a Typeahead search. We’ve used concatMap on multiple occasions, in Avoiding Nested Subscriptions in Angular and also Populating Multiple Dropdowns in Angular Using forkJoin. Today we’ll be letting switchMap show its stuff and why it’s so useful in a situation like this – where the user will be changing their search term frequently, essentially every time they are done typing.

So, let’s jump right in. First thing we’ll need is Angular Material. We’ll be using the material autocomplete form control for our Typeahead. It takes care a lot of the details for us so it’s ideal for this example. Plus it looks sexy. (Again, the woman and or men are NOT going to want you for implementing a Typeahead in your code – I can’t stress this enough.)

We install Angular Material like so:

>> ng add @angular/material

I go into Angular Material in a bit more detail in this article Displaying a Loading Dialog on an Angular Parent Component While Child Components Load, but for now, just select yes to everything and pick a color theme. I’m going to get nutty and select Deep Purple/Amber for our example, but you pick whatever you want.

Let’s modify our app module to import the Angular Material items we need. Let’s also add Forms and Reactive Forms as we’ll use those later as well. Add these imports and also add them to the imports array:

App Module (app.module.ts)

import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatInputModule } from '@angular/material/input';
import { MatAutocompleteModule } from '@angular/material/autocomplete';

.....

  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    FormsModule,
    ReactiveFormsModule,
    MatFormFieldModule,
    MatInputModule,
    MatAutocompleteModule
  ],

Now let’s start on our app. We’re going to create a system whereby new superheroes in the Marvel universe can see what superhero names are still available. (There’s not many after 15 years.) Once they select an available name from our Typeahead, they’ll be directed to a detail page where they can see more information about the name, how much it costs, their origin story, what superpowers they will gain, etc.

I’m fairly certain this is not how new superheroes come about. But let’s get going!

To start, we’ll create a service to return the available superhero names for our users to choose from. In the real world, this would obviously be a database operation where our API would submit a query to our backend to retrieve the list of names and eventually detail information. However, in this article we’re just going to focus on the Typeahead, so in our service we’ll simply return a JSON object array. We’ll also filter that array on items that contain any of the characters in the search term the user has entered.

Let’s create that very service:

>> ng g s type-ahead

And the contents:

Typeahead service (type-ahead.service.ts)

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

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

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

    const mockJson = [

      { id: '1', value: 'The Visible Girl', disabled: false },
      { id: '2', value: 'Anxiety Man', disabled: false },
      { id: '3', value: 'Acne Boy', disabled: false },
      { id: '4', value: 'The Human Goiter', disabled: false },
      { id: '5', value: 'Vegan Zombie', disabled: false },
      { id: '6', value: 'Windows Update', disabled: false },
      { id: '7', value: 'JavaScript Diaper', disabled: false },
      { id: '8', value: 'The Incredible Middle Manager', disabled: false },
      { id: '9', value: 'Commander Reflux', disabled: false },
      { id: '10', value: 'Urinal Mint', disabled: false },
      { id: '11', value: 'Pause Squat', disabled: false },
      { id: '12', value: 'The Turd', disabled: false },
      { id: '13', value: 'Extended Warranty', disabled: false }


    ];

    return of(mockJson.filter(item =>
      item.value.toLowerCase().indexOf(searchValue.toLowerCase()) > -1));

  }

}

So again, this would be a database call in a real app, and the filtering would occur there, but for our example we’re just going to return the list of available names and filter on the variable “searchValue” passed to our service method getDropDownData. Note each item has a value of “disabled: false” added to it. You’ll see why in a bit.

Our app also is going to have a search form and a detail form as the landing page once a search item is selected in our Typeahead. We’ll use Angular Routing and set up our app component form with pretty much nothing but some text and a router outlet:

App component (app.component.html)

<h1>The Big List of MCU Unused Superhero Names.</h1>
<h2>Please Select Your New Identity!</h2>
<router-outlet></router-outlet>

Now let’s create our 2 components, the search and detail:

>> ng g c type-ahead  

>> ng g c detail-page

And then one last item before the fun begins, let’s add our routing with a default route of our Typeahead search component:

App routing module (app-routing.module.ts)

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DetailPageComponent } from './detail-page/detail-page.component';
import { TypeAheadComponent } from './type-ahead/type-ahead.component';

const routes: Routes = [

  {
    path: 'TypeAhead',
    component: TypeAheadComponent
  },
  {
    path: 'DetailPage',
    component: DetailPageComponent
  },
  { path: '',   
    redirectTo: '/TypeAhead', 
    pathMatch: 'full' }, 
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Fabulous, let’s get Typeaheading! (That’s not a word.) We’ll go ahead and enter the guts of our Typeahead form, then we’ll talk about it bit by bit:

Typeahead form (type-ahead.component.html)

<div>
    <form>
        <mat-form-field style="margin-left: 20px; width: 300px;">
            <mat-placeholder>Type some characters, future hero</mat-placeholder>
            <input matInput [formControl]="searchBox" [matAutocomplete]="ac" id="searchBox" />

            <mat-autocomplete #ac="matAutocomplete" (optionSelected)="itemSelected($event)">
                <mat-option *ngFor="let option of searchResults; " [disabled]="option.disabled" [value]="option.id">
                    {{ option.value }}
                </mat-option>
            </mat-autocomplete>
        </mat-form-field>
    </form>
</div>

So let’s look at the 2 main parts of our form. First we’re wiring up a mat-form-field wrapper for everything. The first item within is a placeholder, which just looks impressive, and also gives some rudimentary instruction, in this case “Type some characters, future hero”

        <mat-form-field style="margin-left: 20px; width: 300px;">
            <mat-placeholder>Type some characters, future hero</mat-placeholder>

Next the actual matInput for our textbox to type in:

<input matInput [formControl]="searchBox" [matAutocomplete]="ac" id="searchBox" />

We also wire our input up to a formControl in the code behind called “searchBox”, so we can access it programmatically. Finally we wire up this input to our auto complete element which we’ll discuss next. This is done by setting the [matAutoComplete] property of the matInput to a template reference of “ac” (though you could use any name):

[matAutocomplete]="ac"

This is the template reference we’ll specify on our autocomplete so the two can interact. And that brings us to our Angular Material auto complete item:

            <mat-autocomplete #ac="matAutocomplete" (optionSelected)="itemSelected($event)">
                <mat-option *ngFor="let option of searchResults; " [disabled]="option.disabled" [value]="option.id">
                    {{ option.value }}
                </mat-option>
            </mat-autocomplete>

As stated, we’ll assign the autocomplete a template reference of “#ac” so our input can talk to it. We’ve also got a method tied to the optionSelected event of the auto complete. This fires, (you guessed it future hero!) when one of the autocomplete items is clicked on. That’s where we’ll determine what item was selected and route to our detail form. The remainder is a pretty typical ngFor, which will loop through each item of our searchResults array and bind them to the autocomplete. However, do note we’re also setting the disabled attribute of each item to the disabled property of each object in our JSON array. Again, you’ll see why this is when we get into the code behind, but every item from our service has “disable = false”, so they are all selectable.

Let’s see what the fruit of our thus far labor looks like. In order to get this code to compile, temporarily add these 3 items to the component class in the typescript file.

Typeahead component (type-ahead.component.ts)

export class TypeAheadComponent implements OnInit {

  public searchBox = new FormControl();
  public searchResults: any[] = [];

  itemSelected(event): void {
  }

We’ll delete all that in a minute, but now our code will run, and you can see what our form looks like in all its glory, albeit with no items being returned when we type:

Angular Typeahead example

You can see the placeholder already knows how to behave and gives a very nice effect when we click in the input and type something. Go ahead and delete the items we temporarily put into the component, and we’ll start building the real component.ts file a piece at a time. Let’s start with all the class variables we’ll need:

Typeahead component (type-ahead.component.ts)

export class TypeAheadComponent implements OnInit {

  public searchBox = new FormControl();
  public clearedBox = true;
  public searchTerm = '';

  // Object bound to autocomplete
  public searchResults: any[] = [];

  // Empty default object for when the search box doesn't have anything in it
  public defaultResults: any[] = [];


  // JSON to display when no items are returned from the service
  public noResult: any = {
    id: 0,
    value: 'No names match criteria',
    disabled: true

  };
  public noResults: any[] = [];

We start by declaring our FormControl called searchBox which is the input on our form. Then we create a Boolean called clearedBox (more on that in a bit) and a string to hold our searchTerm called, um, searchTerm (duh.) Next searchResults is the array that is bound to our autocomplete and will be assigned the values returned from our service. We then set up 2 more arrays:

  • defaultResults which is an empty array we use to initilze the list to nothing.
  • noResults which is what we want to display if the user types something that has NO match in our list. (Note disabled is set to true for this item. Again you’ll see why in a minute.)

We’ll pass 2 services into the constructor, the router service and of course our Typeahead service:

  constructor(
    private _TypeAheadService: TypeAheadService,
    private _router: Router
  ) { }

Now onto the ngOnInit method and all the nougaty goodness that makes this form work. Here’s the entire ngOnInit – we’ll break it down bit by bit after:

  ngOnInit(): void {

    // Add our not found item to the noResults array
    this.noResults[0] = this.noResult;
    // set the inital value of searchResults to our empty array
    this.searchResults = this.defaultResults;
    // Setup a debounce time constant of 1/2 a second.
    const DEBOUNCE_TIME = 500;


    // get values from the service
    this.searchBox.valueChanges.pipe(
      debounceTime(DEBOUNCE_TIME),
      switchMap(changedValue => {
        if (changedValue.trim() !== '') {
          this.searchTerm = changedValue.trim();
          this.clearedBox = false;
          return this._TypeAheadService.getDropDownData(this.searchTerm);
        } else {
          this.searchTerm = '';
          this.clearedBox = true;
          return of(this.defaultResults);
        }
      }
      )
    ).subscribe(data => {
      if (data.length === 0) {
        if (this.clearedBox) {
          this.searchResults = data;
        } else {
          this.searchResults = this.noResults;
        }
      } else {
        this.searchResults = data;
      }
    }
    );
  }

Before we call our service, we do some simple things like adding our noResult object to the noResult array, set searchResults to defaultResults (blank) and also setup a constant for debounce time. (I put the constant definition right above the service call for readability, you’d put it in the class variables normally).

And now the actual subscription. We’re going to subscribe to an event called valueChanges of our searchBox control. In Angular, FormControls (and some other form objects) have a really useful event exposed called valueChanges. valueChanges is an observable that we can subscribe to so that each time the value of the textbox changes we can do stuff, in this case, filter our autocomplete list of items.

Starting with valueChanges, we pipe the results:

this.searchBox.valueChanges.pipe(

We pipe the results to the debounceTime operator. This guy is a lifesaver. It allows you to wait a certain amount of time before emitting. In our case we set this to a half a second, using our constant:

debounceTime(DEBOUNCE_TIME),

Now if the user fires valueChanges again (i.e., types something else within half a second), the timer is reset and the half a second count begins again. Why do we do this? Well imagine if we didn’t do this – Our autocomplete would fire off populating a new list after every character entered, as the user types. If there were a delay in the results being returned as would be typical in a real-world app, we’d be firing off API calls left and right before any of them had a chance to return results, putting more stress on the backend / db than needs be. By using debounce, we create a magical, pleasant experience where the webform essentially says to the user – “Ok, are you done typing? Since it’s been half a second, I think you are, and I’ll go populate the autocomplete list now. Also have a great day!”

Now we need to emit this value (what the user has typed) to a new observable. We do this using a higher order mapping operator. In our case switchMap is going to work great with our Typeahead. switchMap unsubscribes from any prior inner Observable and subscribes to any new inner Observable. This has the very desirable effect of stopping any prior inner Observable before processing the new one, exactly what we want in this case. In other words, if switchMap could speak English it would say something like – “Hey, we got a new value emitted here, whatever you were doing – just throw it out the window and process this new value. Also, have a great day!” (Our sentient code creatures are apparently very polite.)

switchMap(changedValue => {

We’ve called our debounced value “changedValue” and we’ll just simply check that the change wasn’t the user clearing the textbox out. Also, If the user cleared out the text or typed something that had no matches, we’ll handle all that shortly.

if (changedValue.trim() !== '') {

Passing that check, we know the user actually did type something, so let’s do a few things. First, we’ll trim the value:

this.searchTerm = changedValue.trim();

Next we’ll set the variable we have called “clearedBox” to false. We use this Boolean to indicate if the user cleared out the text box or not and handle that in the subscribe. Since we checked for a value above in our if, we know this is false and the user hasn’t cleared the textbox:

this.clearedBox = false;

Finally we pass the search term to the service and return the Observable to our subscribe:

return this._TypeAheadService.getDropDownData(this.searchTerm);

Otherwise (else), the user did indeed clear the box. In that case, we set searchTerm to “”, set clearedBox to true, and return an Observable of defaultResults which again we simply defined earlier as an empty array:

} else {
  this.searchTerm = '';
  this.clearedBox = true;
  return of(this.defaultResults);
}

Finally, the subscribe:

).subscribe(data => {

So, we’ve called our service and MAYBE gotten some data back, we don’t know yet – the user could have typed complete gibberish into the search box, or some characters that don’t match any name in our list. So let’s check if the service returned any values:

if (data.length === 0) {

Now you’ll see what “clearedBox” is used for. Recall a bit back, prior to our subscribe, we’re returning one of 2 observables. Either the result set from our service (if the user actually typed something) or our default result (if the user cleared out the textbox). If the latter, we also set clearedBox to true. Thanks to that good work, when we subscribe, we will now automatically get the appropriate observable plus we’ll know if the user cleared the textbox. So all we really need to check for now is did the service return any data, i.e., is the length 0 or not. The reason being- we want to differentiate if the service returned no rows (the user typed something that didn’t return any names) or the user cleared the textbox. If we set clearedBox to true earlier, the user indeed cleared the checkbox, and we simply display the defaultResults we are returning automatically in the observable. If clearedBox is false and the service data.length = 0, we know the service didn’t return any matching rows. In that case, we want to display the noResults array (the message saying, “No names matched”.) This way we’re not telling them the service returned no results if they cleared the textbox, which would be annoying. So if they cleared the textbox, display the observable, otherwise display the noResults message:

if (this.clearedBox) {
  this.searchResults = data;
} else {
  this.searchResults = this.noResults;
}

Else (if we have a data.length that doesn’t equal 0) the user typed a search term that had some matches, the service indeed returned some data and that’s what we’ll display in the Material autocomplete:

 } else {
  this.searchResults = data;
 }
}

Now the data our autocomplete display is all taken care of, for the last bit of our component, we’ll handle the itemSelected event and route to our detail page, passing along the id of the selected item, utilizing the state navigation option.

itemSelected(event): void {
    this._router.navigate(['/DetailPage'], { state: { selectedId: event.option.value } });
}

Ok. Let’s have a look at all this working! But first let’s code up our simplified detail component as that’s the final piece we’ll need. The component code:

Detail component (detail-page.component.ts)

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-detail-page',
  templateUrl: './detail-page.component.html',
  styleUrls: ['./detail-page.component.scss']
})
export class DetailPageComponent implements OnInit {
  selectedId: number;

  constructor(private _router: Router) {

    this.selectedId = this._router.getCurrentNavigation().extras.state.selectedId;
   }

  ngOnInit(): void {
  }
}

In our itemSelected() event handler in the Typeahead form, we passed along the option value that was selected using a variable we called selectedId.
Our detail page does nothing more than grab this variable from the navigation state and assign it to a local variable of the same name, selectedId. In a real-world app, you’d take this primary key, and run a query to return detail information of the item they selected. In our unused superhero name app, we’d requery the database to get all the specifics on the name they selected, such as what superpowers it comes along with, their origin story, pricing, etc. and ultimately a way to purchase the name. But again, that’s beyond the scope of the Typeahead that’s being demonstrated in this article, so our detail form will simply display the key of the item selected:

Detail form (detail-page.component.html)

<h3>You selected the item with id of {{ selectedId }} !!</h3>

And with that, our unused MCU superhero name generator lookup thingy is ready to roll! Let’s fire off the app and see how everything functions. Start the app and type the word “the” into our input:

Angular Typeahead example

Excellent – you can see the values are filtered on anything that contains the word “the”. Notice how the debounce of 1/2 second is also working. If you type “the” one letter at a time very slowly, you can see the list is refreshed for every letter you type. But if you type “the” somewhat quicker, the debounce ensures that the list is only retrieved when you are done typing. (haven’t touched a key for the 1/2 second, firing the valueChanges() event.) Debounce makes for a pleasant UX experience and prevents our API from getting called more than necessary. Win / win for all.

Back in our app, go ahead and select “The Incredible Middle Manager” (No doubt his superpowers would include scheduling meetings and micromanaging in a single bound):

And voila, we have the primary key (id) of the item selected in the previous form by our Typeahead! Let’s do a few more things to see some of the remaining functionality we coded. First off, return to the type ahead form and type some gibberish, “xyz” or anything that won’t return any results:

Angular Typeahead example

Remember how we set disabled: true on our no results item? Now you can see why – the user gets a message that states nothing was found for that search string, but it’s not something they can select. There’s a couple of ways to handle a no results message, but I like this way for a few reasons. First, it’s not a lot of code and second, the message shows up in the autocomplete, exactly where the user expects their results to show up. If we put “No names found” in a separate label somewhere else on the screen, it would be a bit jarring, and I like consistency. So with the simple addition of “disabled” true or false to each item, we can display whatever we want in the results AND prevent the user from clicking on informational messages. But feel free to handle this message differently if you care to. Finally, go ahead and clear out what you typed in the box:

Angular Typeahead example

Notice how we don’t get the “No names found” message any longer. We wouldn’t want to tell the user nothing was found every time they deleted the text from the input. Our user might start screaming uncontrollably “Duh, I haven’t typed anything yet!” and perhaps throwing office furniture, which would suck.

Note: If you grab this code from my GitHub, I’ve included comments in the Typeahead component as to where you would open and close a loading dialog so the user would get a “Please wait…” type message while the data is retrieved:

      switchMap(changedValue => {
      // Close Loading Panel
        if (changedValue.trim() !== '') {
          this.searchTerm = changedValue.trim();
          // Open Loading Panel

We had no need for these dialogs with our no latency app and they are outside the scope of the article but have a read here Displaying a Loading Dialog on an Angular Parent Component While Child Components Load for all the loading dialog goodness you can handle.

For your reading pleasure:

Higher Order Observables (switchMap at the bottom)
https://blogs.msmvps.com/deborahk/higher-order-observable/

Navigation Behavior Options (state) https://angular.io/api/router/NavigationBehaviorOptions#state

Our good friend debounce
https://rxjs.dev/api/operators/debounce

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

Simple Change Detection on Angular Forms

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

We talked about preventing navigation when a form has uncommitted data a few articles back Prevent Navigation from Forms with Uncommitted Data – Angular Love Fest but what’s a good way to detect form changes so we know if we should prevent navigation or not? If you’re ever bored and think “I hope there’s 40 or 50 different ways to accomplish this”, then detecting form changes is a great topic for you to read up on!

However, today I’ll talk about a way I’ve done form change detection in the past and I like it because it’s proven quite effective over time, it’s fairly simple, and doesn’t require a lot of exotic things going on.

While this technique is fairly simple, there are some gotchas you need to be aware of that we’ll code for, so do read the whole thing. First thing we’ll need is, duh, a form. So, let’s make that component:

>> ng g c my-form

And let’s add our HTML to the form:

My Form HTML (my-form.component.html)

<form [formGroup]="myFormGroup">
    <div class="container">
        <div class="row">
            <div class="col-2"><label for="myName">Name</label></div>
            <div class="col-2"><input type="text" id="myName" 
             name="myName" formControlName="myName"></div>
        </div>
        <div class="row mt-2">
            <div class="col-2"><label for="active">Active</label></div>
            <div class="col-2"><input type="checkbox" id="active"
             name="active" formControlName="active"></div>
        </div>
        <div class="row mt-2">
            <div class="col-2"><label for="department">Department</label>
            </div>
            <div class="col-2"><select id="department" 
             name="department" formControlName="department">
                <option value="">-- Please Select --</option>
                <option value="1">Sales</option>
                <option value="2">IT</option>
                <option value="3">HR</option>
                </select>

            </div>
        </div>
        <div class="row mt-2">
            <div class="col-2"><label for="notes">Notes</label></div>
            <div class="col-2"><textarea id="notes" name="notes"
             formControlName="notes"></textarea></div>
        </div>
        <div class="row mt-2">
            <div class="col-2"><button (click)="saveForm()">Save</button>
            </div>
        </div>
    
    </div>

So, we’ve got a textbox, a checkbox, a dropdown and a text area just to cover some of the more common inputs. We also have a button to save the form as we’ll have some things to do in the SaveForm() method. Since the purpose of this article is to demonstrate if any changes in the form have taken place, what we really need as well is a link to navigate off the page. Then we can do some testing to see what happens when a user changes some of the form values and tries to leave the page. Let’s add that link at the very bottom of the above html:

    <div class="container">
        <div class="row mt-4">
            <div class="col-2">
                    <a href="javascript:void(0)"
                     (click)="tryToLeavePage()">Leave page</a>
            </div>
        </div>
    </div>

Now let’s put the basics in our My Form component file so we can run the app:

My Form Component (my-form.component.ts)

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms'


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

  myName: FormControl;
  active: FormControl;
  department: FormControl;
  notes: FormControl;

  myFormGroup: FormGroup;


  constructor() { }

  ngOnInit(): void {

    this.myName = new FormControl('');
    this.active = new FormControl('');
    this.department = new FormControl('');
    this.notes = new FormControl('');

    this.myFormGroup = new FormGroup({
      myName: this.myName,
      active: this.active,
      department: this.department,
      notes: this.notes
    })
  }

  saveForm(): void {
    alert('Saved!');
  }

  tryToLeavePage(): void {
    alert('Leaving page!');
  }

}

So fairly straightforward so far, we’ve created FormControls for our html elements and a FormGroup named myFormGroup. Two simple methods just fire off an alert when we do a simulated save or navigate away from the page.
Our humble little ugly form looks like this now:

So let’s wire up our change detector. And it’s really quite simple provided you code for aforementioned gotchas, which we will of course do. Let’s first add a class variable to our component file:

.......
  initialFormValue = '';
.......

And then we’ll set this value using JSON.stringify in the OnInit. Add this code in that method, right after the form group is built:

.......
    this.initialFormValue = JSON.stringify(this.myFormGroup.value);
    console.log(this.initialFormValue);

So, at very end of our Init, we’re setting initialFormValue to the JSON string representation of our form (and outputting it to the console.) In dev tools we can see what that variable looks like:

{"myName":"","active":"","department":"","notes":""}

As expected, a bunch of empty strings. Now let’s wire up some compare logic in our navigate away method. Replace tryToLeavePage() with the following:

  tryToLeavePage(): void {

    console.log('Initial Form: ' + this.initialFormValue);
    console.log('Current Form: ' + 
                 JSON.stringify(this.myFormGroup.value));

    if (this.initialFormValue === 
        JSON.stringify(this.myFormGroup.value)) {
      alert('Leaving page!');
    } else {
      alert('Form has changed, can\'t let you leave!!');
    }
  }

We’ve got 2 log statements so we can see what’s going on and then the guts of the method which essentially compares the JSON string of the initial FormGroup value to the current value of the FormGroup. If they match (i.e., nothing has changed), we simulate the user being allowed to navigate away from the page. If they don’t equal (some form value has changed), we prevent navigation.

If you click the “Leave page” link without making any changes to the form, you can see navigation is permitted. However, change any value in the form and click “Leave page” again and you’ll see the differences in the JSON strings outputted to the console. So, the very basics of this technique are working.

BONUS: One of my favorite aspects of this method is that if the user changes a value on the form, then changes it back to its previous value, it is NOT reported as a change. This is because we are literally comparing values as opposed to checking if a form control is pristine or dirty.

So, in the method I’m describing here, it’s really as simple as that – comparing 2 JSON strings. However, there’s more to handle to make this work properly and we’ll go over those steps one by one in the next section.

NOTE: If you really want a good example of preventing navigation away from a page that is much more comprehensive than the simple button we’re using here, something you can actually use in your apps to truly prevent navigation, including closing the tab / browser, see the article mentioned above: Prevent Navigation from Forms with Uncommitted Data – Angular Love Fest

Step 1: Account for form initial population

Let’s pretend we grabbed some values from a database when the form is loaded, as the user can come back and edit this form as much as they want. As opposed to writing a service, let’s just use consts for this example to represent the values that were retrieved from the database. In our Init method, right before setting initalFormValue, let’s add variables and setValue statements:

......    
    const myNameFromDb: string = 'John Doe';
    const activeFromDb: boolean = true;
    const departmentFromDb: number = 2;
    const notesFromDb: string = 'Some Notes';
   
    this.myFormGroup.controls.myName.setValue(myNameFromDb);
    this.myFormGroup.controls.active.setValue(activeFromDb);
    this.myFormGroup.controls.department.setValue(departmentFromDb);
    this.myFormGroup.controls.notes.setValue(notesFromDb);

If you now try modifying the form and then clicking to navigate away, everything still works. However, let’s pretend this is a new record and there aren’t any values in the database just yet. (Or perhaps only some form values were saved last time). In that case, we’d pull null from the database as well. So let’s set them all to null to see what happens.

    const myNameFromDb: string = null;
    const activeFromDb: boolean = null;
    const departmentFromDb: string  = null;
    const notesFromDb: string = null;

Now if you navigate away immediately, you’ll get the expected response that nothing has changed. However, go ahead and enter a value in name, then delete it. Also click the checkbox, then unclick it. Now the form should be in the same shape as when it was loaded however, we can see our comparison fails:

Initial Form: {"myName":null,"active":null,"department":null,"notes":null}
Current Form: {"myName":"","active":false,"department":"","notes":null}

So, when we modify a textbox control and then clear it out, the form value is not set back to null, but an empty string. And in the case of the textbox, it’s set to false. So even though conceptually nothing has changed on the form, our comparison indicates quite a lot has changed and prevents navigation – not what we want.

The way to handle this is we must exorcise all nulls from our controls when the data is loaded:

    this.myFormGroup.controls.myName.setValue(
         myNameFromDb === null ? '' : myNameFromDb);
    this.myFormGroup.controls.active.setValue(
         activeFromDb === null ? false : activeFromDb);
    this.myFormGroup.controls.department.setValue(
         departmentFromDb === null ? '' : departmentFromDb);
    this.myFormGroup.controls.notes.setValue(
         notesFromDb === null ? '' : notesFromDb);

NOW, load up the form, change all 4 values, then change them back. As we would expect, we’re now allowed to leave the form as nothing has really changed!

Big Sidebar:

Before we get to the next step, I do need to bring up a possible situation when saving these values to the database. While we’re not really using a database in this example and we’re not going to write any code for our save button in this example, this does need to be mentioned as it might affect your code. That depends 100% on the business rules of your app. Now at the shop I work at, once a user saves a form, if they don’t enter a value into a textbox, it gets saved to the database as an empty string, as conceptually at least, that is the value they saved. And this has never caused any issues for us. If that’s your case, you can skip to Step 2 or read on just in case you ever face this situation. Also, if you have the choice, I would highly recommend going this way.

However, your buiness rules might dictate that if the user saves a form, and hasn’t entered a value or clicked the checkbox that a null value be saved to the database. And that’s just fine. You just have a bit more coding to do in our hypothetical save routine, essentially checking if each control has been “touched” or not, which Angular conveniently exposes for us. Let’s use myName as an example to demonstrate this. Temporarily, change the code of the saveForm to this:

  saveForm(): void {

    const changed = (this.myFormGroup.controls.myName.value !==
                     JSON.parse(this.initialFormValue)['myName']);

    alert(' myName changed? ' + changed + ' myName touched? ' 
         + this.myFormGroup.controls.myName.touched);

    alert('Saved!');
  }

Now load up the form and click save. You’ll see that the control reports as not having been changed and not having been touched, exactly what we’d expect.
Now, enter some text into the name box, then delete it, restoring the myName control to blank. Now click save and you’ll see that changed still reports as false, but touched now reports as true and these are the checks you’d need to make in order to determine if a null should be written to the database:

If a control has no value AND the user hasn’t touched it, you’d record a null to your db.

As to how you’d have to apply this to other controls such as a checkbox is why this method doesn’t make a whole lot of sense to me. Using the same methodology – if a control is blank and the user hasn’t touched it then it’s null:

If a checkbox field in the database has a null value, when that value is loaded, the checkbox is going to show as “false” on the form even though the user hasn’t touched it yet. So, using the above logic, when saved, a null would get written back to the database as the checkbox is still false and it hasn’t been touched.

But what if the user wants it to be false? In this case, they’d have to click it true, so the checkbox control registered as having been touched, but then they’d have to click it again to set it to false! You can imagine how angry a user would be to have to be trained and remember to do that. So, in my opinion, if you have the choice, don’t persist nulls in the database from a form save, but if you must – you know how to check if a user has touched a control.

To continue, we’ll not be saving nulls to our db so restore saveForm() to it’s original simplicity:

  saveForm(): void {
    alert('Saved!');
  }


Step 2: Reset the initialFormValue

Let’s look at the other thing that can bite you in the butt when using this method if you don’t code for it. Load up the form again and change a few values.
Now click Save. The try to leave the page. Well come on, I saved (simulated) the form and it’s still telling me I can’t leave the page!

Now you might have a Save / Submit button that has a redirect at the end of it. However, if you have a Save where the user remains on the page, you’ll need to alter saveForm():

  saveForm(): void {
    this.initialFormValue = JSON.stringify(this.myFormGroup.value);
    alert('Saved!');
  }

Simple fix, but easy enough to miss. The idea being once the user saves the form, the initial values do need to be reset to whatever they saved so the navigation check works properly. Now make a bunch of changes, save the form, then try to navigate away. Victory! We get the behavior we expect. Just one final step which is super simple but also important.

Step 3: Only Reset initialFormValue if the Form is Valid

Our form controls haven’t had any validators up to this point and that would be unusual outside of this example in the real world. Let’s add a validator to name to finish this off:

    this.myName = new FormControl('', Validators.required);

With the validator added, we can now show that you don’t want to reset initialFormValue just any old time the user clicks save but only if the save operation was permitted, i.e., the form was valid and actually saved. So, let’s modify saveForm one final time:

  saveForm(): void {
    if (this.myFormGroup.valid) {
    this.initialFormValue = JSON.stringify(this.myFormGroup.value);
    alert('Saved!');
    } else
    { 
      alert('Form is not valid! (enter a name)');
    }
  }

And now we’re done! You can see by now changing some values in the form, we’re only permitted to leave once save is clicked AND the form is valid, as that’s the only time the initialFormValue is reset:

So hopefully you find this technique useful. There are just a few caveats to code for, but we end up with a pretty simple and fairly durable way to determine if there’s been a form change quickly AND to isolate only true changes to the form.

Read please:

All things Angular forms including form control states, like dirty, touched, and valid: Angular – Building a template-driven form

Prevent Navigation from Angular Forms with Uncommitted Data

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

Problem: My users are stupid and keep navigating away from a form with changed or dirty data or closing the browser altogether then complain because none of their changes are showing up.

Your users are not stupid, they’re just busy with jobs that aren’t writing software. We all face this common problem with end users. They either:

  • navigate away from a form with uncommitted / dirty data.
  • close the browser or tab when a form has uncommitted / dirty data.

Typically, this is followed by blaming the app for not saving their changes. While we have some options here, such as forms that save every so often,
today we’ll look at a pretty typical solution – a simple confirmation prior to navigation or closing the browser / tab. There are several approaches to this, but the following is how I’ve implemented this several times and I’ve found it to be pretty durable. If you have a different approach, feel free to share it or improve upon this one.

We’ll need to hit this from two angles, using an Angular route guard for in app navigation and then hooking to a DOM event in case the user tries to close a tab or the entire window.

NOTE – If you’re using lazy loading with your modules (as we do where I work), there’s some changes you’ll need to make which I’ll go over at the very end of the article.

So let’s get started – we’ll create 2 components in our app, one that will utilize the dirty form logic, the other will be a separate route so we can see the navigation behavior in action:

>> ng g c dirty-form
>> ng g c other-route

Now we’ll need to set up a route to each component:

(app-routing.module.ts)

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DirtyFormComponent } from './dirty-form/dirty-form.component';
import { OtherRouteComponent } from './other-route/other-route.component';

const routes: Routes = [
  {
    path: 'DirtyForm',
    component: DirtyFormComponent,
  },
  {
      path: 'OtherRoute',
      component: OtherRouteComponent      
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Ok, super simple, we have an app with 2 components and the ability to route between them. Let’s do some modifications to our dirty-form.component files.

Dirty Form HTML (dirty-form.component.html)

<p>Form goes here.</p>

Amazing levels of realism I know, but in actuality we don’t need a form to demonstrate what we’re doing here, just a placeholder.


Earlier I stated we’d need to attack this from two angles, and for both we’ll need an Angular route guard. Route guards, if you’ve never used one are interfaces provided by Angular that allow us to control access to our routes. There are several types which you will read up on at the end of the article. For our purpose, thankfully Angular provides just the type of route guard we need called CanDeativate. Let’s create that very thing in a new file:

Route Guard (dirty-form-guard.ts)

import { CanDeactivate } from '@angular/router';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

export interface ComponentCanDeactivate {
  canDeactivate: () => boolean | Observable<boolean>;
}

@Injectable()
export class DirtyFormGuard implements CanDeactivate<ComponentCanDeactivate> {
  canDeactivate(component: ComponentCanDeactivate): boolean | Observable<boolean> {
    return component.canDeactivate() ? true :
      confirm('WARNING: You have unsaved changes. Press Cancel to go back and save these changes, or OK to lose these changes.');
  }
}

End result of this – we’ll be able to use a boolean (say formUnchanged or something of the like) to determine if a user can navigate away from a page or close the browser window. This is powerful stuff and makes it very easy to implement on every form in our app. Total win.

So how do we use this newfound power? First, let’s make sure we provide it in our app module:

App Module (app.module.ts)

....
  providers: [DirtyFormGuard],
...

And now in our dirty form component file, let’s wire up the guard:

Dirty Form Component (dirty-form.component.ts)


import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { ComponentCanDeactivate } from '../dirty-form-guard';

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

  formUnchanged = false;

  public canDeactivate(): Observable<boolean> | boolean {
    
    return this.formUnchanged;

  }


  constructor() { }

  ngOnInit(): void {
  }

}

So 2 things in our DirtyFormComponent, notice we implement the interface from our route guard, ComponentCanDeactivate:

export class DirtyFormComponent implements OnInit, ComponentCanDeactivate {

This is good practice and ensures that we provide the guts for (or an implementation of) the required method canDeactivate():


  public canDeactivate(): Observable<boolean> | boolean {
    
    return this.formUnchanged;

  }

Now in this case we are doing something very simple – we have a variable called formUnchanged (set to false) as a placeholder to indicate that a user has made a change to the form on the page.

This is in the interest of sanity. I’d prefer a series of smaller articles that address a single need rather than something that’s 1700 pages long. The reason being is HOW to tell if a user has truly made a change to a form is an entire discussion in itself (which everyone seems to have their own approach to)
and will be covered in its own article which I will link to here: Simple Change Detection on Angular Forms – Angular Love Fest


Give that a good read when you have some time, but for now let’s simply concentrate on the topic at hand of how to prevent navigation when the form has changed and for that purpose – our little boolean will do just fine.

So our final step in preventing in app routing is to assign this route guard to whatever components we care to. In this case, we’ll attach it to our dirty form component in our app routing file:


................
  {
    path: 'DirtyForm',
    component: DirtyFormComponent,
    canDeactivate: [DirtyFormGuard]
  }
...............

I mentioned earlier that we can use this logic in every form in our app and you can see how easy it is to use this guard on whatever routes we want. Let’s see what happens now when we start our app and try to navigate away from our dirty form component:

Fabulous! You can see anytime we click on the “Dirty Form” route then try to navigate to “Other Route”, our confirmation box fires and forces us to hit ok to proceed (or cancel to stay). Also note if we set our formUnchanged variable to true in the dirty form component, this simulates a user having made no changes to the form and navigation proceeds without any prompting.

So that’s the first part of the battle – note that you can still close the tab or close the browser entirely and the prompt doesn’t occur, leaving a way for our users to lose uncommitted data. Closing the page / tab is a bit tricker (but not super difficult) to handle and for that we’ll have to hook into the DOM of the web browser. Fortunately, angular makes this very straightforward.

Begins yelling at cloud
If you don’t know what the DOM (document object model) is, you’re likely to have not been doing much web programming in the 90s. Back when I had to walk uphill to school both ways and the only game on my phone was Snake, we had to deal with DOM of various web browsers…
End yelling at cloud moment

tl;dr – there’s objects and events in your web browser itself that, even though we are very much shielded from them these days thanks to all the wonderful JavaScript frameworks, we still need to access once in a while. And I said Angular makes that very straightforward and indeed they do with a decorator called @HostListener.

From Angualr.io: (@HostListener) Decorator that declares a DOM event to listen for and provides a handler method to run when that event occurs.

EXACTLY what we need and the DOM event in question that we want to listen for is ‘window:beforeunload’:

From Mozilla: The beforeunload event is fired when the window, the document and its resources are about to be unloaded.

So let’s listen for that DOM event and provide a method to run when that event occurs. Make this simple change in dirty-form.component.ts by adding the @HostListener decorator before our canDeactivate() method:

Dirty Form Component (dirty-form.component.ts)

  @HostListener('window:beforeunload')
  public canDeactivate(): Observable<boolean> | boolean {
    
    return this.formUnchanged;

  }

Now, back in our app, in addition to getting our confirmation prompt when we try to navigate away from the dirty form page, if we try to close the tab or the browser itself:

Now we’ve got our users right where we want them! (not really but count the small victories) If we determine there is uncommitted data, we can now both prevent navigating away from the page and prevent closing the tab or browser without confirmation.

NOTE – You’ll note the message is different when they try to close the browser. Why is that? The short answer – most modern browsers don’t support custom messages in the beforeunload popup. I don’t find this very troubling as the message is pretty clear. You can also copy this message and use it in your canDeativate method if you need them to be consistent.

IF YOU’RE USING LAZY LOADED MODULES
We use lazy loaded modules where I work and I had real problems making this work at first, it just wouldn’t work, or I’d get null component error.
Let me save you the hair pulling on this one – it’s mentioned here in this issue at the Angular github:
https://github.com/angular/angular/issues/16868 (About 1/3 of the way down – picninim commented on Sep 20, 2017 – explains it).


Luckily, it’s not a difficult change, but if you’re using lazy loaded modules you need to move the canDeactive from the app-routing.module.ts to the lazy loaded modules’ routing file, in our example, if we used lazy loaded modules, that would be in the file dirty-form-routing.module.ts:

Dirty Form Routing Module (dirty-form-routing.module.ts)

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DirtyFormGuard } from '../dirty-form-guard';
import { DirtyFormComponent } from './dirty-form.component';

export const CHILD_ROUTES: Routes = [{

  path: '',
  component: DirtyFormComponent,
  canDeactivate: [DirtyFormGuard]
}];

 
@NgModule({
  imports: [
    RouterModule.forChild(CHILD_ROUTES)

  ],

  declarations: [],
  exports: [
    RouterModule
  ]

})

export class DirtyFormRoutingModule { }

And you’d follow the above pattern for each lazy loaded module.

You go read now:

Angular route guards
https://codeburst.io/understanding-angular-guards-347b452e1892

Host listener
https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event