close icon
Angular

Migrating an AngularJS App to Angular - Part 2

Learn how to migrate real-world features of an AngularJS 1 application to a fresh Angular 2+ build (Part 2): routing, API, and filtering.

November 09, 2016

Check out the Real-World Angular Series to learn how to build and deploy a full-featured MEAN stack application

, from ideation to production! Start the series here: Real-World Angular Series - Part 1: MEAN Setup and Angular Architecture .

The Branding Guidelines for Angular state that version 1.x should be referred to as AngularJS, whereas all releases from version 2 and up are named Angular. This migration article will continue to use "Angular 1" to refer to AngularJS (1.x) and "Angular 2" to refer to Angular (2 and up) in order to clearly differentiate the frameworks and reduce confusion.

TL;DR: Many AngularJS 1.x developers are interested in Angular 2, but the major differences between versions 1 and 2 are daunting when we have so many Angular 1 apps already in production or maintenance. In the first part of this tutorial we set up our Angular 2 app and migrated the basic architecture. This time we'll implement some real-world features like routing, calling an API, and more. The final code for our Angular 2 app can be cloned from the ng2-dinos GitHub repo.


Recap and Introduction to Part 2

In Migrating an Angular 1 App to Angular 2 - Part 1 we introduced the Angular 1 ng1-dinos Single Page Application and began migrating it to Angular 2 ng2-dinos. So far we've migrated global styles, custom off-canvas navigation, header, and footer. This part of this tutorial will cover:

  • Page components and routing
  • Using a service to call the sample-nodeserver-dinos API
  • Displaying all dinosaurs in a list view
  • Filtering dinosaurs by query

Note: Remember that we're migrating an Angular 1 app to Angular 2 with a fresh build. We're not upgrading the original Angular 1 codebase.

Setup and Dependencies

Part 2 has the same dependencies as Part 1 of our tutorial, so make sure you have:

We'll pick up right where we left off.

Migrating Angular 2 Pages

In Part 1, we implemented some links in our navigation, but we don't have pages to display when the links are clicked. Let's create some components so we can implement routing.

Create Home, About, and 404 Page Components

In order to implement routing, the first thing we need is multiple pages. Let's quickly create home, about, and 404 components. These will be pages so create a subdirectory in the ng2-dinos/src/app/ folder called pages. Stop the server and execute the following commands:

  • Home page component: $ ng g component pages/home
  • About page component: $ ng g component pages/about
  • 404 page component: $ ng g component pages/error404

Add Title Provider to App Module

We want to update the document <title> tag for each page. Recall that <title> is outside the <app-root> element in the document <head>, but Angular 2 provides a useful service to set the title.

We want the Title service to be registered in the root injector so it's available to the entire application. Let's add it to our app.module.ts:

// ng2-dinos/src/app/app.module.ts

import { BrowserModule, Title } from '@angular/platform-browser';
...

@NgModule({
  ...
  providers: [
    Title
  ]
})
export class AppModule { }

To learn more about this, read the Angular 2 docs on Dependency Injection.

Add Title to Page Components

The page components should each display a heading and update the <title> with the Title service we provided in the step above. Let's implement this in each of our new page components. We don't have to provide Title at the component level (@Component({ providers: [Title]...) because we're providing it at an application level in app.module.ts (above).

Open the home.component.ts file and make the following changes:

// ng2-dinos/src/app/pages/home/home.component.ts

import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
  pageName = 'Dinosaurs';

  constructor(private titleService: Title) { }

  ngOnInit() {
    this.titleService.setTitle(this.pageName);
  }

}

First we'll import the Title class from @angular/platform-browser. In our HomeComponent class, we'll create a pageName property and set it to "Dinosaurs". Then we'll add the private titleService: Title to our constructor function. In our ngOnInit() function, we'll set the title to the pageName. You can consult the Angular 2 docs to learn more about the Title service.

Now let's do the same for the about and 404 components: about.component.ts and error404.component.ts. We'll also delete the about.component.scss and error404.component.scss files and any references to them. The about and 404 components will be plain pages with some static copy. We can use Bootstrap classes to style both and don't need componetized SCSS.

Now open the about component about.component.ts:

// ng2-dinos/src/app/pages/about/about.component.ts

import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';

@Component({
  selector: 'app-about',
  templateUrl: './about.component.html'
})
export class AboutComponent implements OnInit {
  pageName = 'About';

  constructor(private titleService: Title) { }

  ngOnInit() {
    this.titleService.setTitle(this.pageName);
  }

}

Finally we'll update the error404 component error404.component.ts:

// ng2-dinos/src/app/pages/error404/error404.component.ts

import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';

@Component({
  selector: 'app-error404',
  templateUrl: './error404.component.html'
})
export class Error404Component implements OnInit {
  pageName = '404 Page Not Found';

  constructor(private titleService: Title) { }

  ngOnInit() {
    this.titleService.setTitle(this.pageName);
  }

}

Home Component Template

Now we have a document title but we also want to display pageName in a heading in our HTML. Let's write some basic markup.

In the home.component.html file, add an <article> and a heading with an interpolated binding to display pageName.

<!-- ng2-dinos/src/app/pages/home/home.component.html -->
<article id="content-wrapper" class="content-wrapper">
  <h2 class="content-heading">{{pageName}}</h2>
</article>

We'll add a lot more to this component later.

About Component Template

Let's add some basic information about our app in the about.component.html template:

<!-- ng2-dinos/src/app/pages/about/about.component.html -->
<article id="content-wrapper" class="content-wrapper lead">
  <h2 class="content-heading">{{pageName}}</h2>
  <p><strong>ng2-dinos</strong> is a sample application built with Angular 2 with the following features:</p>
  <ul>
    <li>Routing</li>
    <li>Dynamic <code>&lt;title&gt;</code> metadata</li>
    <li>External <code>GET</code> API</li>
    <li>Custom off-canvas navigation</li>
    <li>Filtering by predicate</li>
    <li>Bootstrap</li>
    <li>SCSS</li>
    <li>Angular CLI (Webpack) build</li>
  </ul>
  <p>Download the code for this app from the <a ng-href="http://github.com/auth0-blog/ng2-dinos">ng2-dinos GitHub repo</a>. The API can be downloaded from the <a ng-href="http://github.com/auth0-blog/sample-nodeserver-dinos">sample-nodeserver-dinos repo</a>. The purpose of this project is to demonstrate an AngularJS 1.x app (<em>without</em> backported v2 features) "migration"/translation to Angular 2.</p>
</article>

404 Component Template

This component will show when the route the user attempts to access does not exist. We'll apply a couple of Bootstrap classes in the error404.component.html template:

<!-- ng2-dinos/src/app/pages/error404/error404.component.html -->
<article id="content-wrapper" class="content-wrapper">
  <h2 class="content-heading text-danger">{{pageName}}</h2>
  <p class="lead">The page you are attempting to access does not exist.</p>
</article>

Note: Our Angular 1 ng1-dinos app had classes like .home-wrapper and .about-wrapper on the article elements but Angular 2's view encapsulation negates the need for this!

Migrating Angular 1 Routing to Angular 2

Routing is an essential feature of our ng1-dinos app. For ng2-dinos, we're going to create a new @NgModule to handle routing. This gives us more flexibility to expand routing later, if needed, without bloating the app.module.ts.

Create a Routing Module

Because of how the CLI generates multiple files per component in its own subdirectory, sometimes it's more straightforward to create a new feature manually. Regardless, we should know how to do this. Let's create a routing module in the ng2-dinos/src/app/core/ folder. We'll name this file app-routing.module.ts:

// ng2-dinos/src/app/core/app-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';

import { HomeComponent } from '../pages/home/home.component';
import { AboutComponent } from '../pages/about/about.component';
import { Error404Component } from '../pages/error404/error404.component';

@NgModule({
  imports: [
    RouterModule.forRoot([
      {
        path: '',
        component: HomeComponent
      },
      {
        path: 'about',
        component: AboutComponent
      },
      {
        path: '**',
        component: Error404Component
      }
    ])
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule {}

At its heart, this doesn't look much different from the Angular 1 route config at ng1-dinos/src/app/core/app.config.js. We declare a path and a component that should display when routed to that path. We need to import the RouterModule as well as any components we want to use. The wildcard path ** should be the last one.

Note: You can read more about routing in the Angular 2 docs. At time of writing, the docs are the most reliable source of information on the Angular 2 router. When searching for blog articles or Stack Overflow answers, be mindful of publish dates and versioning: the Angular 2 router was one of the last pieces to reach completion and has undergone rewrites and breaking changes throughout the beta and release candidate phases.

Let's take a quick break to verify our ng2-dinos/src/app file structure:

ng2-dinos
  |-src/
    |-app/
      |-core/
        |-app.component[.html|.scss|.ts]
        |-app-routing.module.ts
      |-header/
        |-_nav.scss
        |-header.component[.html|.scss|.ts]
      |-footer/
        |-footer.component[.html|.scss|.ts]
      |-pages/
        |-about/
          |-about.component[.html|.ts]
        |-error404/
          |-error404.component[.html|.ts]
        |-home/
          |-home.component[.html|.scss|.ts]
      |-app.module.ts

Import Routing Module in App Module

We have a new module to handle routing but it isn't being imported anywhere in our app right now. We need to add it to our app.module.ts:

// ng2-dinos/src/app/app.module.ts

...
import { AppRoutingModule } from './core/app-routing.module';
...

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Import the AppRoutingModule class and then add it to the imports array.

Display Routed Components

Routing is now configured! Now we just need to display our routed components in the view. In Angular 1, this was done with the ng-view directive. In Angular 2, we'll add the <router-outlet> element where we want our page components to display in our app.component.html template:

<!-- ng2-dinos/src/app/core/app.component.html -->
...
    <!-- CONTENT -->
    <div id="layout-view" class="layout-view">
        <router-outlet></router-outlet>  
    </div>
...

If we serve and view the app in the browser, we should see the home component when we visit http://localhost:4200.

Route Navigation

Right now, we don't have any live links to our routes. We still need to make some updates to the header.component.html to enable route navigation and active link highlighting.

Our Angular 1 ng1-dinos app header controller had to utilize a custom navIsActive(path) function to compare the URL path with the link href to apply an active class in the navigation markup. The Angular 2 router can do this for us!

Open the header.component.html file and let's make some changes to the first two links in the menu:

<!-- ng2-dinos/src/app/header/header.component.html -->
...
    <ul class="nav-list">
      <li>
        <a
          routerLink="/"
          routerLinkActive="active"
          [routerLinkActiveOptions]="{ exact: true }">Dinosaurs</a>
      </li>
      <li>
        <a routerLink="/about" routerLinkActive="active">About</a>
      </li>
      ...

In Angular 1, we used the ng-href directive. In Angular 2, we'll use the routerLink directive instead. We can also add routerLinkActive="[active-class-name]" and Angular 2 will automatically apply our desired class to the link when that route is active.

Note: The caveat is that this needs an additional option when dealing with the root URL. The routerLinkActive directive returns a match if the routerLink is contained in the URL tree. This means that routerLink="/" is also matched by all other routes with a / in them. To enable exact matching, we need to add [routerLinkActiveOptions]="{ exact: true }" to our root link.

Now we should be able to click the links in the off-canvas menu and be routed appropriately with proper active link classes. Try it out.

Router Events

You probably noticed that there's still one thing missing that ng1-dinos had: automatic navigation closing on route change. We definitely don't want to manually close the off-canvas menu every time we switch pages.

In ng1-dinos, we used $scope.$on('$locationChangeStart', ...) in the navControl directive to bind a handler and close the menu. Something similar exists in Angular 2, so let's implement it!

Auto-close Menu in Header Component

We'll do this in our header.component.ts file where we emitted the event earlier to notify the app component parent. This way we can ensure that both components know about the change and the nav states don't get out of sync:

// ng2-dinos/src/app/header/header.component.ts

...
import { Router, NavigationStart } from '@angular/router';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {
  ...
  constructor(private router: Router) { }

  ngOnInit() {
    this.router.events
      .filter(event => event instanceof NavigationStart && this.navOpen)
      .subscribe(event => this.toggleNav());
  }
  ...

We need to import Router and NavigationStart from @angular/router. Next we need to make private router: Router available in our constructor function.

Router.events is an observable of route events. We'll filter for when the event is an instance of NavigationStart and the navigation is open. We'll then subscribe to it to set navOpen to false.

Now when we click on links in the menu the correct component displays and the navigation closes. Our app homepage now looks like this:

Migrating AngularJS app to Angular: Angular 2 single page application with routing

Calling an API in Angular 2

Now our architecture and navigation is in place! We've arrived at the business logic portion of our app. Angular 1 ng1-dinos used a service to call the API: ng1-dinos/src/app/core/Dinos.service.js.

We're going to author a service for this in our Angular 2 migration too. Let's start by creating the file. Use the following Angular CLI command to create a service boilerplate:

$ ng g service core/dinos

When we run this command, note the warning output informing us that the service was generated but not provided. We'll provide it at the component level this time instead of application-wide like we did with the Title service. This means we won't put DinosService in the app.module.ts.

The purpose of DinosService is to call the API and get dinosaur information. To do this, we'll use HTTP observables. We also need to create TypeScript models for our fetched data.

Dinosaur API Data Model

Let's create a model for the data we're going to retrieve for the main listing of dinosaurs. In order to do this, we need to know the format of the API response. We can determine this simply by making an API request in the browser (and consulting the sample-nodeserver-dinos API README).

The API route we want to use is http://localhost:3001/api/dinosaurs. Assuming you have the API running locally, let's access this route in the browser and look at the response:

Note: You may want to install/enable a JSON formatting browser extension to view the response.

// http://localhost:3001/api/dinosaurs

[
  {
    "id": 1,
    "name": "Allosaurus"
  },
  {
    "id": 2,
    "name": "Apatosaurus"
  },
  {
    "id": 3,
    "name": "Brachiosaurus"
  },
  ...
]

We can see that the response is an array of dinosaur objects. Each dinosaur has an id and a name. We can see the id is a number and the name is a string. Now we can create a model.

We'll have more than one model, so let's create a folder for models to keep our app scalable: ng2-dinos/src/app/core/models/. In this folder, we'll make our model file: dino.model.ts.

export class Dino {
  constructor(
    public id: number,
    public name: string
  ) { }
}

Add HTTP Client Module to App Module

Now we have the "shape" of a dinosaur defined. Let's work on getting the data from the API.

First we need to import the HttpClientModule in our app.module.ts:

// ng2-dinos/src/app/app.module.ts

...
import { HttpClientModule } from '@angular/common/http';
...
@NgModule({
  ...,
  imports: [
    ...,
    HttpClientModule
  ],
  ...

Import HttpClientModule from @angular/common/http and then add it to the NgModule's imports array.

Get API Data with Dinos Service

Next let's fetch the API data in our dinos.service.ts:

// ng2-dinos/src/app/core/dinos.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';

import { Dino } from './models/dino.model';

@Injectable()
export class DinosService {
  private baseUrl = 'http://localhost:3001/api/';

  constructor(private http: HttpClient) { }

  getAllDinos$(): Observable<Dino[]> {
    return this.http
      .get(`${this.baseUrl}dinosaurs`)
      .catch(this.handleError);
  }

  private handleError(err: HttpErrorResponse | any) {
    let errorMsg = err.message || 'Unable to retrieve data';
    return Observable.throw(errorMsg);
  }

}

This is pretty straightforward and it doesn't look that much different from our ng1-dinos Dinos service. Aside from Angular 2 format, the primary difference is that we're returning typed observables instead of promises (and we haven't added the API call to get a single dinosaur's details by id yet—we'll do that later).

Starting from the top: we import our dependencies. Services are injectable. The CLI adds the Injectable class for us. We also need HttpClient and HttpErrorResponse from @angular/common/http, Observable from RxJS, and the catch operator. Finally we need our Dino model.

Note: RxJS observables are preferable over promises. Angular 2's http.get returns an observable but we could convert it to a promise with .toPromise() if we had to (but we won't in this tutorial).

We set our private API baseUrl property and make private http: HttpClient available in the constructor function.

Then we define our getAllDinos$() function. The $ at the end of the function name indicates that an observable is returned and we can subscribe to it. The getAllDinos$(): Observable<Dino[]> type annotation declares that we expect an array of items matching the Dino model we created previously.

Finally we manage successes and errors. The map operator processes the result from the observable. In our case, we're returning the response as JSON. We'll use the catch operator to handle failed API responses and generate an observable that terminates with an error.

Note: In the Angular 1 ng1-dinos Dinos service, the success function checks for an object because some server configurations (such as NGINX) will return a successful XHR response with an HTML error page in the case of an API failure. The front-end promise incorrectly resolves this as the appropriate data. We do not need to do this check in Angular 2 ng2-dinos because we have TypeScript ensuring that the shape of the data matches our Dino model. Pay attention to your data though: if you have a response that occasionally changes shape, you'll need to address that in the model so you don't receive errors. You can read more about TypeScript functions and optional parameters here.

Provide the Dinos Service in App Module

We want the dinos service to be a singleton. Unlike Angular 1, Angular 2 services can be singletons or have multiple instances depending on how they're provided. To create a global singleton, we'll provide the service in the app.module.ts:

// ng2-dinos/src/app/app.module.ts

...
import { DinosService } from './core/dinos.service';

@NgModule({
  ...,
  providers: [
    ...,
    DinosService
  ],
  ...

We import the DinosService and then add it to the providers array. It's now available for use in our components.

Use the Dinos Service in Home Component

Now we have a service that fetches data from the API. We'll use this service in our home component to display a list of dinosaurs. Open the home.component.ts file:

// ng2-dinos/src/app/pages/home/home.component.ts

...
import { DinosService } from '../../core/dinos.service';
import { Dino } from '../../core/models/dino.model';

@Component({
  ...
})
export class HomeComponent implements OnInit {
  dinos: Dino[];
  error: boolean;
  pageName = 'Dinosaurs';

  constructor(
    private titleService: Title,
    private dinosService: DinosService) { }

  getDinos() {
    this.dinosService
      .getAllDinos$()
      .subscribe(
        res => {
          this.dinos = res;
        },
        err => {
          this.error = true;
        }
      );
  }

  ngOnInit() {
    this.titleService.setTitle(this.pageName);
    this.getDinos();
  }

}

As always, we import our dependencies. We need our new DinosService and Dino model.

Then we'll implement the functionality to use this service. We'll declare that the dinos property should be of type Dino[] (an array of items matching the Dino model). We'll also create an error boolean property. We'll add the private dinosService: DinosService to the constructor parameters.

We can then write the getDinos() method to subscribe to the getAllDinos$() observable and assign the response to the dinos property. In the function for error handling, we'll set the error property to true.

Finally, we'll call the getDinos() method in our ngOnInit() function.

Display a List of Dinosaurs

We now have dinosaur data available, we just need to render it in the home.component.html template. We'll start by displaying it in a simple unordered list. We also want to show an error if something goes wrong retrieving data from the API:

<!-- ng2-dinos/src/app/pages/home/home.component.html -->
...
  <!-- Dinosaurs -->
  <ul *ngIf="dinos">
    <li *ngFor="let dino of dinos">{{dino.id}} - {{dino.name}}</li>
  </ul>
  <!-- Error -->
  <p *ngIf="error" class="alert alert-danger">
    <strong>Rawr!</strong> There was an error retrieving dinosaur data.
  </p>
...

The ng-repeat of Angular 1 has been replaced by the ngFor repeater directive.

Note: The * asterisk before ngIf and ngFor is syntactic sugar that allows us to skip wrapping subtrees in <template> tags. You can read more about

.

We now have a list of all the dinosaurs returned from the API. Our app homepage looks like this in the browser:

Migrating AngularJS app to Angular: Angular 2 app showing list with API data

We can also test the error state by stopping the local Node dinos server and then reloading our Angular 2 app. We should see this:

Migrating AngularJS app to Angular: Angular 2 app showing data error

Display Dino Cards

Our Angular 1 ng1-dinos app repeats a dinoCard directive with a template that displays each dinosaur's name and detail link in a card styled with Bootstrap. The implementation in ng2-dinos will be similar.

We'll start by generating the new dino card component in the same folder as our home component:

$ ng g component pages/home/dino-card

Dino Card Component TypeScript

The dino card won't have to do much processing, but we want to use the @Input decorator to give it dinosaur data. Let's set this up in the dino-card.component.ts:

// ng2-dinos/src/app/pages/home/dino-card/dino-card.component.ts

import { Component, Input } from '@angular/core';

import { Dino } from '../../../core/models/dino.model';

@Component({
  selector: 'app-dino-card',
  templateUrl: './dino-card.component.html'
})
export class DinoCardComponent {
  @Input() dino: Dino;
}

We need to import Input from @angular/core. We also need our trusty Dino model. Then we'll declare our @Input() dino: Dino typed property. We don't need to add anything to the constructor so the constructor() { } function can be deleted. We also aren't using the OnInit lifecycle hook so we can remove it from imports, the exported class, and the ngOnInit() function. Keep in mind that if we expand functionality at some future date, we may need to replace things we've cleaned up for brevity.

Dino Card Component Template

Let's create the template for the dino card component. This file will be very similar to the ng1-dinos dino card template:

<!-- ng2-dinos/src/app/pages/home/dino-card/dino-card.component.html -->
<div class="dinoCard panel panel-info">
  <div class="panel-heading">
    <h3 class="panel-title text-center">{{dino.name}}</h3>
  </div>
  <div class="panel-body">
    <p class="text-center">
      <a class="btn btn-primary" href>Details</a>
    </p>
  </div>
</div>

Notice that the Details button doesn't go anywhere yet. We'll hook this up when we add the dinosaur detail component and routing.

Display Dino Card in Home Component Template

Now let's replace the unordered list with our new dino card component in home.component.html:

<!-- ng2-dinos/src/app/pages/home/home.component.html -->
...
  <!-- Dinosaurs -->
  <section *ngIf="dinos" class="row">
    <div class="col-xs-12 col-sm-4" *ngFor="let dino of dinos">
      <app-dino-card [dino]="dino"></app-dino-card>
    </div>
  </section>
...

We'll add some Bootstrap classes so our cards display nicely in a grid. Then we'll implement the <app-dino-card> element in our repeater. We'll pass dino data to it with property binding.

Our ng2-dinos homepage now looks like this:

Migrating AngularJS app to Angular: Angular 2 app showing child component cards with API data

Our migration is coming together. The Angular 2 app is finally starting to look more like ng1-dinos!

Migrating Angular 1 Filtering to Angular 2

You may have heard about Angular 2 pipes. Pipes transform displayed values within a template. In Angular 1, we used the pipe character (|) to do similar things with filters. However, filters are gone in Angular 2.

No Filter or OrderBy Pipes

In our Angular 1 ng1-dinos app, we could filter our dinosaurs repeater by binding an ng-model="query" to an input and then using item in array | filter: query on the repeater. This is no longer built-in in Angular 2. The Angular 2 team recommends against replicating this functionality with a custom filtering pipe due to concerns over performance and minification.

Instead, we'll create a service that performs filtering. You may already be familiar with filtering this way on Angular 1 apps with large amounts of data where performance becomes an issue. Angular 1 apps can slow to a crawl if care isn't taken with how filtering is handled. If you've ever had to search hundreds or thousands of items or implemented faceted search, you should be familiar with the pitfalls and workarounds.

Note: How is a filtering service different from a custom pipe? Filtering lists is very expensive. With a service, we can control when and how often the filtering logic is executed. You can read more in the "No FilterPipe or OrderByPipe" section of the Pipes docs (at the very bottom).

Create a Filter Service

Let's create a service for filtering:

$ ng g service core/filter

We want our filter service to provide a search() method that accepts an array and a query string. It should check objects in the array for strings that contain the query and return a new array of all objects with a match. Let's implement this in filter.service.ts:

// ng2-dinos/src/app/core/filter.service.ts

import { Injectable } from '@angular/core';

@Injectable()
export class FilterService {
  search(array: any[], query: string) {
    const lQuery = query.toLowerCase();

    if (!query) {
      return array;
    } else if (array) {
      const filteredArray = array.filter(item => {
        for (const key in item) {
          if ((typeof item[key] === 'string') && (item[key].toLowerCase().indexOf(lQuery) !== -1)) {
            return true;
          }
        }
      });
      return filteredArray;
    }
  }

}

We want search to be case-insensitive so we'll convert the query and values to lowercase when checking for matches. If the method is called with a falsey query, we'll return the original array instead of trying to check for matches. For our ng2-dinos search, we're only going to check string values in the objects. If you need a more robust search (ie., one that also checks dates, numbers, etc.) you'll want to handle that specifically. This is one of the benefits of implementing filters this way over the old Angular 1 filter: we have more fine-grained control.

Now that we have a way to filter by query, let's implement this in our home component.

Filter in Home Component TypeScript

Open the home.component.ts file:

// ng2-dinos/src/app/pages/home/home.component.ts

...
import { FilterService } from '../../core/filter.service';

@Component({
  ...
  providers: [DinosService, FilterService]
})
export class HomeComponent implements OnInit {
  dinos: Dino[];
  filteredDinos: Dino[];
  error: boolean;
  pageName = 'Dinosaurs';
  query = '';

  constructor(..., private filterService: FilterService) { }

  getDinos() {
    this.dinosService.getAllDinos$()
      .subscribe(
        res => {
          this.dinos = res;
          this.filteredDinos = res;
        },
        err => {
          this.error = true;
        }
      );
  }

  ngOnInit() {
    this.titleService.setTitle(this.pageName);
    this.getDinos();
  }

  filterDinos() {
    this.filteredDinos = this.filterService.search(this.dinos, this.query);
  }

  resetQuery() {
    this.query = '';
    this.filteredDinos = this.dinos;
  }

  get noSearchResults() {
    return this.dinos && !this.filteredDinos.length && this.query && !this.error;
  }

}

We need to import and then provide our FilterService. Next we'll set its parameter in the constructor function. Now we can use it in our home component.

Note: By providing the filter service in the component instead of app.module.ts, we're creating an instance unique to this component. We're doing this here because there is only one place we're filtering. If you add filters to additional components in the future, consider using a global singleton if there's no compelling reason to create multiple instances.

We're going to create a property called filteredDinos alongside our dinos property. The filtered collection should also have the Dino[] type. When we successfully retrieve data from the API, we'll set filteredDinos as well as dinos. At this point it is the full collection.

Next we need a method for the template to use to filter the dinosaur list. We'll call this method filterDinos(). Inside this function, we'll pass the query and our full dinos collection to the FilterService method we created and set its results: this.filteredDinos = this.filterService.search(this.dinos, this.query).

Our ng1-dinos app has a way to instantly clear the search with a button. We want the same feature in ng2-dinos, so let's create a resetQuery() method. This method sets the query to an empty string and then sets filteredDinos to the original, unfiltered dinos array. The reason we have to manually reset the array is because we're going to declaratively run filterDinos() on keyup in the query input field. This won't be triggered when the user clicks the button to clear the query.

Finally, we need a method that returns an expression informing the template that no search results match the query. If there is a dinos array, the filteredDinos array is empty, there is a query, and (as a catch-all), there is no API error, then we can conclude the user's search has produced no results. In our ng1-dinos app, we used this expression in the ng-if in the view. Angular 2 recommends shifting logic of this type into the component.

Filter in Home Component Template

You can reference the Angular 1 ng1-dinos Home.view.html to check out the markup for searching. We're going to copy and then modify it for ng2-dinos home.component.html:

<!-- ng2-dinos/src/app/pages/home/home.component.html -->
...
  <!-- Search dinosaurs -->
  <section *ngIf="dinos" class="home-search input-group">
    <label class="input-group-addon" for="search">Search</label>
    <input
      id="search"
      type="text"
      class="form-control"
      [(ngModel)]="query"
      (keyup)="filterDinos()" />
    <span class="input-group-btn">
      <button
        class="btn btn-danger"
        (click)="resetQuery()"
        [disabled]="!query">&times;</button>
    </span>
  </section>
  <!-- Dinosaurs -->
  <section *ngIf="dinos" class="row">
    <div class="col-xs-12 col-sm-4" *ngFor="let dino of filteredDinos">
      <app-dino-card [dino]="dino"></app-dino-card>
    </div>
  </section>
  <!-- No search results -->
  <p *ngIf="noSearchResults" class="alert alert-warning">
    No information available on a dinosaur called <em class="text-danger">{{query}}</em>, sorry!
  </p>
...

We want to use two-way binding with ngModel to bind the query to the search input. On the keyup event, we'll run our filterDinos() function. This will update the filteredDinos array. We also have a button to clear the search query. On click, we'll execute resetQuery(). If there's no query, we can disable the button.

Note: ngModel now requires the FormsModule from @angular/forms. The Angular CLI creates new projects with this dependency in app.module.ts automatically but it's important to know why and how we utilize it in our app.

In order for our filtering to work in the template, we need to update the *ngFor repeater to use the filteredDinos array instead of the dinos array.

We also want to show a message if a user searches and there are no matching results. This message should show if the noSearchResults getter returns true.

Filter in Home Component Styles

If we view our app, you may notice we could use a bit of styling to put some space between the search and the dinosaur list. Open the home.component.scss file and add:

/* ng2-dinos/src/app/pages/home/home.component.scss */

/*--------------------
         HOME
--------------------*/

.home-search {
  margin-bottom: 20px;
}

We should now be able to search for dinosaurs by name:

Migrating AngularJS app to Angular: Angular 2 app with search filtering

If the search doesn't return any results, we should see a message:

Migrating AngularJS app to Angular: Angular 2 app with search filtering

Aside: Refactoring Suggestions

Here is my refactoring suggestion from part two of our migration tutorial:

Note: You may want to wait to refactor until you complete all parts of the tutorial.

Conclusion

Our ng2-dinos app now calls an API and supports searching! We've successfully migrated the main dinosaurs listing, dino cards, and search form. We've covered HTTP observables and building a filtering service. Make sure you've run ng lint and corrected any issues. With clean code, we shouldn't have any errors.

In the final part of the tutorial, we'll create a dinosaur detail component with routing and we'll show loading states while waiting for API calls to complete.

Migrating an existing application can be a great way to learn a new framework or technology. We experience familiar and new patterns and implement real-world features. Please join me again for the final lesson: Migrating an AngularJS App to Angular - Part 3!

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon