Chapter 9. Building a single-page application with Angular: Foundations

This chapter covers

  • Working with the Angular router and navigating between pages
  • Architectural best practices for an SPA
  • Building up views through multiple components
  • Injecting HTML into bindings
  • Working with browsers’ native geolocation capabilities

You saw in chapter 8 how to use Angular to add functionality to an existing page. In this chapter and chapter 10, you’ll take Angular to the next level by using it to create a single-page application (SPA). Instead of running the entire application logic on the server using Express, you’ll run it all in the browser using Angular. For some benefits and considerations when using an SPA instead of a traditional approach, flick through chapter 2. By the end of this chapter, you’ll have the framework for an SPA in place with the first part up and running by using Angular to route to the homepage and display the content.

Figure 9.1 shows where you are in the overall plan, recreating the main application as an Angular SPA.

Figure 9.1. This chapter recreates the Loc8r application as an Angular SPA, moving the application logic from the back end to the front end.

In a normal development process, you probably wouldn’t create an entire application on the server and recreate it as an SPA. Ideally, your early planning phases defined whether you wanted an SPA, enabling you to start in the appropriate technology. For the learning process you’re going through now, it’s a good approach; you’re already familiar with the functionality of the site, and the layouts have already been created. This approach lets you focus on the more exciting prospect of seeing how to build a full Angular application.

In this chapter, you’ll start by adding the Angular router to navigate between pages; then, you’ll create the homepage and the About page and add geolocation functionality. As you add more components and functionality, you’ll explore various best practices, such as making reusable components and building up a modular application.

9.1. Adding navigation in an Angular SPA

In this section, you’ll add the outline of the About page and enable navigation between this new page and the homepage. The main focus of this section is the navigation; you’ll complete the About page in section 9.4.

You may remember that when you configured the Express application, you defined URL paths (routes) and used the Express router to map the routes to specific pieces of functionality. In Angular, you’ll do the same thing but use the Angular router instead.

One big difference in using the Angular router is that the full application is already loaded in the browser, so when you navigate between pages, the browser doesn’t fully download all the HTML, CSS, and JavaScript each time. Navigating becomes a much quicker experience for the user; the only things they normally have to wait for are data from API calls and any new images.

The first step is importing the Angular router into the application.

9.1.1. Importing the Angular router and defining the first route

The Angular router needs to be imported into app.module.ts, which is also where you’ll define the routes. The router is imported from @angular/router as RouterModule, which should be placed with the other Angular imports at the top of app.module.ts.

Listing 9.1. Adding the RouterModule to the list of imports in app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';            1

  • 1 Imports the Angular RouterModule

In the same file, in the @NgModule decorator, all these modules are listed in the imports section. You need to do the same with RouterModule, but in this case, you also need to pass it the routing configuration you want.

9.1.2. Routing configuration

The routing configuration is an array of objects, each object specifying one route. The properties for each route are

  • pathThe URL path to match
  • componentThe name of the Angular component to use

The path property shouldn’t contain any leading or trailing slashes, so instead of /about/, you’d have about, for example. It can also be an empty string to denote the homepage. Remember that the base href is set in the index.html file? You set yours to be "/", as you want everything running at the top level, but even if you set it to have a value, that value wouldn’t make any difference to the routing configuration. In your routing configuration, you should leave out anything set in the base href html tag.

You start by adding the configuration for the homepage, so path is an empty string, and component is the name of your existing component: HomeListComponent. The configuration is passed to a forRoot method on the RouterModule.

Listing 9.2. Adding the routing configuration to the decorator in app.module.ts
@NgModule({
  declarations: [
    HomeListComponent,
    DistancePipe
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    RouterModule.forRoot([                1
      {
        path: '',                         2
        component: HomeListComponent      3
      }
    ])
  ],
  providers: [],
  bootstrap: [HomeListComponent]
})

  • 1 Adds the RouterModule to the imports, calling the forRoot method
  • 2 Defines the homepage route as an empty string
  • 3 Specifies the HomeListComponent as the one to use for this route

You’ve imported the Angular RouterModule into your application and told it which component to use for the homepage. You can’t test it, however, because you’re also specifying the same component as the default component. Note the line bootstrap: [HomeListComponent] in listing 9.2. What you need to do is create a new default component, which you’ll use to hold the navigation.

9.1.3. Creating a component for the framework and navigation

To hold the navigation elements, you need to create a new component and make that the default component for the application. You’ll also use this component to hold all the framework HTML, much as you did in layout.pug in Express. In reality, the framework HTML is three things: navigation, content container, and footer.

First, create a new component called framework by running the following in terminal from the app_public directory:

$ ng generate component framework

This command creates a new framework folder inside app_public/src/app/ and also generates all the files you need. Find the framework.component.html file, and add all the HTML shown in the following listing, which is pretty much what the HTML content of layout.pug would look like if converted to HTML

Listing 9.3. Adding the HTML for the framework in framework.component.html
<nav class="navbar fixed-top navbar-expand-md navbar-light">        1
  <div class="container">
    <a href="/" class="navbar-brand">Loc8r</a>
      <button type="button" data-toggle="collapse" data-target=
      "#navbarMain"class="navbar-toggler">
        <span class="navbar-toggler-icon"></span>
      </button>

    <div id="navbarMain" class="navbar-collapse collapse">
      <ul class="navbar-nav mr-auto">
        <li class="nav-item">
          <a href="/about/" class="nav-link">About</a>
        </li>
      </ul>
    </div>
  </div>
</nav>
<div class="container content">                                     2
  <footer>                                                          3
    <div class="row">
      <div class="col-12">
        <small>&copy; Getting Mean - Simon Holmes/Clive Harber 2018</small>
      </div>
    </div>
  </footer>
</div>

  • 1 Sets up the navigation section
  • 2 Creates the main container
  • 3 Nests the footer inside the main container

Now that you’ve got the component set up, you need to tell the application to use it as the default component, and tell it where to put it in the HTML.

To set the new framework component as the default component, update the bootstrap value in app.module.ts like so, replacing HomeListComponent with FrameworkComponent:

bootstrap: [FrameworkComponent]

Finally, you need to update index.html to have the correct tag for this component rather than home-list. Open framework.component.ts, and find the selector in the decorator, which gives you the name of the HTML tag you should use:

@Component({
  selector: 'app-framework',
  templateUrl: './framework.component.html',
  styleUrls: ['./framework.component.css']
})

So app-framework is the tag you need to have in index.html so Angular knows where to put the framework component. Update index.html to look like the following.

Listing 9.4. Updating index.html file to use the new framework component
<body>
  <app-framework></app-framework>        1
</body>

  • 1 Replaces the home-list component for the app-framework

Now that your framework component is created and linked to the HTML, you can check it out in the browser, as shown in figure 9.2. If you haven’t done so already, remember to run nodemon from the root folder of the application to get the API running, and also run ng serve from the app_public folder to get the development version of the Angular app running.

Figure 9.2. Showing the framework component by default instead of the listing

You can see the page header displaying, so you have success of sorts. Your new component works! But you don’t see any content, even though you’re on the homepage route. If you open the JavaScript console in the browser, you see an error: Cannot find primary outlet to load 'HomeListComponent'.

You’ve told the application to load HomeListComponent for the homepage route, but haven’t specified where it should be positioned in the HTML.

9.1.4. Defining where to display the content using router-outlet

Specifying the destination for a routed component is as simple as adding an empty tag pair in the HTML where you want it to go. This special tag is <router-outlet>. Angular adds the routed component after this tag, not inside it, as you might expect if you’re familiar with AngularJS.

Adding this empty tag pair to the correct place in the framework HTML—where you had block content in layout.pug—looks like the following.

Listing 9.5. Adding router-outlet to framework.component.html
<div class="container">
  <router-outlet></router-outlet>         1
  <footer>
    <div class="row">
      <div class="col-12"><small>&copy; Getting Mean - Simon Holmes/Clive
     Harber 2018</small></div>
    </div>
  </footer>
</div>

  • 1 Outlet for the router; Angular uses the URL to find the component and injects it here.

If you check out the browser now, you see the listing information as well as the framework. Inspecting the elements, as shown in figure 9.3, demonstrates that <router-outlet> remains empty, and that <app-home-list> was injected afterward.

Figure 9.3. The routed component—the listing information—is now being displayed on the homepage route, with the HTML being injected after the <router-outlet> tag.

You can see the framework and the listing for the homepage, but it’s not the homepage you know and love. It’s missing a header and sidebar. You’ll come back to this page in section 9.2. First, you need to see how the navigation works.

9.1.5. Navigating between pages

To see the navigation in action, update the Angular application so that you can flip between the homepage and the About page. If you click the links right now, they won’t work. To get the navigation working, you need to create an about component, define the about route, and change the links in the navigation to something Angular can use.

Creating the about component with Angular CLI should be familiar by now. In terminal, in the app_public folder, run the following generate command:

$ ng generate component about

This command creates the new component inside app_public/src/app/about. You’ll leave it as it is for now, so you can focus on the navigation. In section 9.4, you’ll return to the About page and build it out fully.

Defining a new route

As with the homepage route, you need to configure the route for the About page in app.module.ts. You need to specify the path for the route as well as the name of the component. The path is 'about'. Remember that you don’t need any leading or trailing slashes.

To make sure you get the name of the component correct, you can open about.component.ts to find it in the export line: export class AboutComponent implements OnInit.

Knowing the path and component name, you can add the new route in app.module.ts.

Listing 9.6. Defining the new about route in app.module.ts
RouterModule.forRoot([
  {
    path: '',
    component: HomeListComponent
  },
  {
    path: 'about',
    component: AboutComponent
  }
])

If you open the browser directly to localhost:4200/about, you get the About page, but the navigation links don’t work properly yet. You’ll fix them in the next section.

Setting Angular navigation links

When you’re using links defined in the router, Angular doesn’t want to see href attributes in the <a> tags; instead, it looks for a directive called routerLink. Angular takes the value you give to routerLink to create the href property.

The rules that apply to defining a path in the router also apply to setting the value for a routerLink. You don’t need to include leading or trailing slashes, and bear in mind that you don’t need to duplicate anything set in the base href.

Following these rules, updating the navigation links in framework.component.html looks like the next listing. Replace href attributes with routerLink directives, ensuring that the values match what you have in the router definition in app.module.ts.

Listing 9.7. Defining the navigation router links in framework.component.html
<a routerLink="" class="navbar-brand">Loc8r</a>             1
<div id="navbarMain" class="navbar-collapse collapse">
  <ul class="navbar-nav mr-auto">
    <li class="nav-item">
      <a routerLink="about" class="nav-link">About</a>      2
    </li>
  </ul>
</div>

  • 1 Empty routerLink path pointing to the default component
  • 2 about path to cause navigation to the about component

With this code in place and saved, you can click between the two links, as shown in figure 9.4.

Figure 9.4. Using the navigation buttons to switch between the homepage and the About page—an Angular SPA!

Notice that the URL in the browser changes as normal, but the page doesn’t reload or flicker when moving between the pages. If you check the network traffic when switching between these two pages, you’ll see only calls to the API being made. You can also use the back and forward buttons in your browser, and the site will work like a traditional website. Congratulations—you’ve built a single-page application!

Before you move on, quickly improve the navigation by adding active styles.

9.1.6. Adding active navigation styles

It’s standard practice in web design to have an active class on navigation items so that the link for the current page looks a bit different—a simple visual cue to tells users where they are. You’ve got only one link in your navigation, but the process is still worthwhile.

Twitter Bootstrap has helper classes defined to create an active navigation state; you set the class active on the active link. As it’s such a common requirement, Angular also has a helper for this class: a directive called routerLinkActive.

On an <a> tag containing a router link, you can add the routerLinkActive directive and specify the name of the class you want to use for active links. You’ll use the class active in framework.component.html:

<a routerLink="about" routerLinkActive="active" class="nav-link">About</a>

The positioning of the routerLinkActive attribute is important. If it doesn’t seem to be working, make sure that you included it before the class attribute.

Now, when you visit the About page, the <a> tag has an extra class of active added to it, which Bootstrap displays as a stronger white color, as you can see in figure 9.5.

Figure 9.5. Seeing the active class in action; Angular adds and removes it from the link as navigation changes are made.

And with that, you’ve covered the basics of the Angular router, creating working navigation for your SPA. You can see that the views clearly need some work, so that’s what you’ll focus on in the next two sections.

9.2. Building a modular app using multiple nested components

In this section, you’ll focus on building out the familiar homepage in Angular. To set yourself up for success—and to follow Angular architectural best practices—you’ll do this by creating several new components and nesting them as you need to. This process gives you a modular application, so you can reuse pieces in different places in the application.

The homepage has three main sections:

  • Page header
  • List of locations
  • Sidebar

You already have the list of locations built as a component; that’s your home-list component. You’ll need to create the header and the sidebar as two new components.

You’ll also need to wrap all three of these components inside a main homepage component to ensure that everything works together, has the correct layout, and can be navigated to via the Angular router. Figure 9.6 shows an overlay of how these components fit together on top of the homepage design. You have the framework component on the outside, holding everything. Nested inside this component is the homepage component to control the content area, with the page header, listing, and sidebar components nested inside it.

Figure 9.6. Breaking the homepage layout into components, using two levels of nesting

This is what you’re going to build. You’ll start with the homepage component.

9.2.1. Creating the main homepage component

The homepage component contains all the HTML and information for the homepage—everything between the header and the footer. This component is what you’ll reference in the router for Angular to use whenever anybody requests the homepage.

Start by using the Angular CLI to generate the component in the now-familiar way (in terminal from the app_public folder):

$ ng generate component homepage

Next, tell the router to use this component for the default home route by updating app.module.ts like so:

RouterModule.forRoot([
  {
    path: '',
    component: HomepageComponent
  },
  {
    path: 'about',
    component: AboutComponent
  }
])

In homepage.component.html, put the selector for the home-list component for a moment before checking it in the browser:

<app-home-list></app-home-list>

If you look at the application in the browser, it looks the way it did before, with the navigation bar, footer, and listing section in between.

But you want to see all the content for the homepage now; that’s the page header, main content, and sidebar. Taking the framework code from the Pug templates and turning it into HTML looks like the following listing. Note that you’re putting the app-home-list component here to display the listing section.

Listing 9.8. Putting the HTML for homepage content in homepage.component.html
<div class="row banner">                  1
  <div class="col-12">
    <h1>Loc8r
      <small>Find places to work with wifi near you!</small>
    </h1>
  </div>
</div>
<div class="row">
  <div class="col-12 col-md-8">           2
    <div class="error"></div>
    <app-home-list></app-home-list>
  </div>
  <div class="col-12 col-md-4">           3
    <p class="lead">Looking for wifi and a seat? Loc8r helps you
    find places to work when out and about. Perhaps with coffee,
    cake or a pint? Let Loc8r help you find the place you're
     looking for.</p>
  </div>
</div>

  • 1 The page header
  • 2 Container for the homepage listing component
  • 3 The sidebar

Now, when you view the page in the browser, you get something like figure 9.7—your good old familiar homepage!

Figure 9.7. The homepage in Angular with the page header and sidebar hardcoded in the homepage component

Everything is there and working correctly, including the home-list component nested inside the homepage component. But you can do better. The page header and sidebar are repeated on other pages, albeit with different text content. You can follow some architectural best practices here and try to avoid repeating code by creating reusable components.

9.2.2. Creating and using reusable subcomponents

You’re going to create the page header and sidebar as new components so that you never need to copy the HTML into multiple views. If the site grows to have dozens or hundreds of pages, you wouldn’t want to have to repeat the same HTML in each layout. This situation gets even worse if you need to update the HTML in the future. It’s much easier to update the HTML in one place, and is also much less prone to errors or omissions.

You’ll make the components “smart” so that you can pass them different content to display. In your case, the reusable components are all about the HTML rather than the content. Start with the page header.

Creating the page-header component

The first step is issuing the familiar component generation command in terminal:

$ ng generate component page-header

Following that command, copy the header content from the homepage HTML and paste it into page-header.component.html:

<div class="row banner">
  <div class="col-12">
    <h1>Loc8r
      <small>Find places to work with wifi near you!</small>
    </h1>
  </div>
</div>

Then you need to reference this content in the homepage.component.html instead of the full HTML currently there. To do so, you need the correct tag, which you can find by looking for the selector in the page-header.component.ts file. In this case, the selector is app-page-header, so that’s what you’ll use in the homepage component HTML.

Listing 9.9. Replacing the page header HTML in homepage.component.html
<app-page-header></app-page-header>
<div class="row">
  <div class="col-12 col-md-8">
    <div class="error"></div>
    <app-home-list>Loading...</app-home-list>
  </div>
  <div class="col-12 col-md-4">
    <p class="lead">Looking for wifi and a seat? Loc8r helps you find
    places to work when out and about. Perhaps with coffee, cake
    or a pint? Let Loc8r help you find the place you're looking
    for.</p>
  </div>
</div>

Good start. You’ve created the new page-header component, but it still has hardcoded content. Next, you’ll pass data to the page header from the homepage component.

Defining the data for the page-header component on the homepage

You want to set the data for the homepage instance of the page-header component from within the homepage component so you can pass it through.

Defining the data is simple. In the homepage component class definition, you create a new member to hold the data. You’ll create a member called pageContent and nest the header inside it, as shown in the next listing. The class member is a simple JavaScript object with text data. Note that the strapline content is shortened in this snippet to save trees.

Listing 9.10. Defining the homepage page header content in homepage.component.ts
export class HomepageComponent implements OnInit {
  constructor() { }

  ngOnInit() {
  }

  public pageContent = {       1
    header: {
      title: 'Loc8r',
      strapline: 'Find places to work with wifi near you!'
    }
  };

}

  • 1 Creates a new class member to hold the page header content

The header is nested inside pageContent because, soon, you’ll add the sidebar content too, and having them both within the same member will keep the code neater. Next, you pass this data to the page-header component.

Passing data into the page-header component

The homepage class member pageContent is now available to the homepage HTML, but rather than use the data directly, you want to pass it through to the page-header component. Data is passed through to the nested component via a special binding in the HTML. The name of the binding is a property you define in the nested component, so it can be anything you want.

You’ll bind the page header content to a property called content. (This property doesn’t exist yet; you’ll define it in the next step.) In homepage.component.html, update <app-page-header> to include the binding:

<app-page-header [content]="pageContent.header"></app-page-header>

Note that although the square brackets may not be valid HTML, that’s okay here, because Angular removes them before serving the HTML to the browser. The actual HTML that the browser will receive is something like <app-page-header_ngcontent-c6="" _nghost-c2="">, which is valid HTML.

You’re now passing data from the homepage component to the nested page-header component; you need to update the page header to accept and use this data.

Accepting and displaying incoming data in a component

You need to tell the pageHeader component that content should exist as a property and to get the value from the outside. Technically, content is an input to the component.

Any property of a class needs to be defined, and this one is no different. Where it differs from what you’ve seen before is that it needs to be defined as an input property. To do that, you need to import Input into the component from the Angular core and use it as a decorator when you define the content member.

Listing 9.11. Telling page-header.component.ts to accept content as an Input
import { Component, OnInit, Input } from '@angular/core';    1

@Component({
  selector: 'app-page-header',
  templateUrl: './page-header.component.html',
  styleUrls: ['./page-header.component.css']
})
export class PageHeaderComponent implements OnInit {

  @Input() content: any;                                     2

  constructor() { }

  ngOnInit() {
  }

}

  • 1 Imports Input from the Angular core
  • 2 Defines content as a class member that accepts an input of any type

When that’s done, the component will understand the data being sent to it from the homepage component, and you’ll be able to display it. Replace the hardcoded text in page-header.component.html with the relevant Angular data bindings.

Listing 9.12. Putting the data bindings in page-header.component.html
<div class="row banner">
  <div class="col-12">
    <h1>{{ content.title }}
      <small>{{ content.strapline }}</small>
    </h1>
  </div>
</div>

Now you have a fully reusable component for the page header, which can display the data sent to it from a parent component. This component is an important building block of Angular application architecture. You’ll cement the process by doing the same for the sidebar so that you can complete the homepage, but you’ll run into a little hiccup along the way.

Creating the sidebar component

We won’t dwell too long on the steps for setting up the sidebar component, as you completed them for the page header earlier in this chapter.

First, generate the component:

$ ng generate component sidebar

Second, grab the sidebar HTML from homepage.component.html, and paste it into sidebar.component.html. When you do, replace the text content with a binding to content:

<div class="col-12 col-md-4">
  <p class="lead">{{ content }}</p>
</div>

Third, allow the sidebar component to receive data by importing Input from Angular core and defining the content property—of type string—with the @Input decorator:

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

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

  @Input() content: string;

  constructor() { }

  ngOnInit() {
  }

}

Fourth, update the pageContent member in homepage.component.ts to contain the sidebar data:

public pageContent = {
  header : {
    title : 'Loc8r',
    strapline : 'Find places to work with wifi near you!'
  },
  sidebar : 'Looking for wifi and a seat? Loc8r helps you find places
  to work when out and about. Perhaps with coffee, cake or a pint?
  Let Loc8r help you find the place you're looking for.'
};

Fifth, update the homepage.component.html to use the new sidebar component, and pass the data through as content:

<app-page-header [content]="pageContent.header"></app-page-header>
<div class="row">
  <div class="col-12 col-md-8">
    <div class="error"></div>
    <app-home-list>Loading...</app-home-list>
  </div>
  <app-sidebar [content]="pageContent.sidebar"></app-sidebar>
</div>

All done! But is it? If you view this page in the browser, you’ll notice that no matter how wide you make your browser window, the sidebar is always below the content (see figure 9.8).

Figure 9.8. The new sidebar component is in and working, but it’s below the main content instead of where it should be.

The position of the sidebar is defined by the classes in the <div class="col-12 col-md-4"> element. But by putting this content inside a component, you wrapped it in a new tag, <app-sidebar>, so Bootstrap is throwing the sidebar below as a new row.

This problem is something to look out for, especially when you’re nesting components. But it’s easy to fix.

Working with Angular elements and Bootstrap layout classes

The problem you have is that the browser now sees this following HTML markup generated:

<div class="col-12 col-md-8">
  <app-home-list>Loading...</app-home-list>
</div>
<app-sidebar [content]="pageContent.sidebar">
  <div class="col-12 col-md-4">
    <p class="lead">{{ content }}</p>
  </div>
</app-sidebar>

The Bootstrap col classes for the sidebar are in the wrong level in the hierarchy, so <app-sidebar> is being treated as a full-width column regardless of browser size. All you need to do is move the classes from the <div> in sidebar.component.html to <app-sidebar> in homepage.component.html, so that homepage.component.html looks like the following.

Listing 9.13. Moving the sidebar classes into homepage.component.html
<app-page-header [content]="pageContent.header"></app-page-header>
<div class="row">
  <div class="col-12 col-md-8">
    <app-home-list>Loading...</app-home-list>
  </div>
  <app-sidebar class="col-12 col-md-4" [content]="pageContent.sidebar">
  </app-sidebar>
</div>

With that done, you no longer need the <div> in the sidebar markup; you can keep the <p> and the content. Now sidebar.component.html looks like this:

<p class="lead">{{ content }}</p>

With that fix, everything should look right with the homepage, as shown in figure 9.9. The homepage is looking good! Something has been missing so far, though. Wouldn’t it be great if Loc8r could tell where you are and find places nearby? You’ll add geolocation to the homepage in the next section.

Figure 9.9. The completed homepage rendering correctly, constructed of multiple nested components

9.3. Adding geolocation to find places near you

The main premise of Loc8r is that it’s location aware and able to find places that are near the user. So far, you’ve been faking it by hardcoding geographic coordinates into the API requests. You’re going to change that right now by adding HTML5 geolocation.

To get geolocation working, you’ll need to do the following things:

  • Add a call to the HTML5 location API to your Angular application.
  • Query the Express API if location details are available.
  • Pass the coordinates to your Angular data service, removing the hardcoded location.
  • Output messages along the way so the user knows what’s going on.

Starting at the top, you’ll add the geolocation JavaScript function by creating a new service.

9.3.1. Creating an Angular geolocation service

The ability to find the location of the user feels like something that would be reusable, in this and other projects. To snap it off as a piece of standalone functionality, you’ll create another service to hold it. As a rule, any code that’s interacting with APIs, running logic, or performing operations should be externalized into services. Leave the component to control the services rather than perform the functions.

To create the skeleton of the geolocation service, run the following in terminal from app_public:

$ ng generate service geolocation

We won’t distract you right now by diving into the details of how the HTML5/JavaScript geolocation API works. Modern browsers have a method on the navigator object that you can call to find the coordinates of the user. The user has to give permission for this to happen. The method accepts two parameters (a success callback and an error callback) and looks like the following:

navigator.geolocation.getCurrentPosition(cbSuccess, cbError);

You’ll need to expose the standard geolocation script in a public method so that you can use it as a service. While you’re here, you’ll also error-trap against the possibility that the current browser doesn’t support this feature. The following listing shows the full code for geolocation.service.ts, providing a public getPosition method that other components can call.

Listing 9.14. Creating a geolocation service using a callback to get current position
import { Injectable } from '@angular/core';

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

  constructor() { }

  public getPosition(cbSuccess, cbError, cbNoGeo): void {              1
    if (navigator.geolocation) {                                       2
      navigator.geolocation.getCurrentPosition(cbSuccess, cbError);    2
    } else {                                                           3
      cbNoGeo();                                                       3
    }
  }
}

  • 1 Defines a public member called getPosition that accepts three callback functions for success, error, and not supported
  • 2 If geolocation is supported, calls the native method, passing through success and error callbacks
  • 3 If geolocation isn’t supported, invokes the not supported callback

That code gives you a geolocation service, with a public method, getPosition, to which you can pass three callback functions. This service checks to see whether the browser supports geolocation and then attempts to get the coordinates. Then the service calls one of the three different callbacks, depending on whether geolocation is supported and whether it was able to obtain the coordinates.

The next step is adding the service to the application.

9.3.2. Adding the geolocation service to the application

To use your new geolocation service, you need to import it into the home-list component, as you did for your data service. You need to do the following:

  • Import the service into the component.
  • Add the service to the providers in the decorator.
  • Add the service to the class constructor.

The following listing highlights in bold the additions you need to make to the home-list component definition to import and register the geolocation service.

Listing 9.15. Updating home-list.component.ts to bring in the geolocation service
import { Component, OnInit } from '@angular/core';
import { Loc8rDataService } from '../loc8r-data.service';
import { GeolocationService } from '../geolocation.service';  1

export class Location {
  _id: string;
  name: string;
  distance: number;
  address: string;
  rating: number;
  facilities: string[];
}

@Component({
  selector: 'app-home-list',
  templateUrl: './home-list.component.html',
  styleUrls: ['./home-list.component.css']
})
export class HomeListComponent implements OnInit {

  constructor(
    private loc8rDataService: Loc8rDataService,
    private geolocationService: GeolocationService            2
  ) { }

  • 1 Imports the geolocation service
  • 2 Passes the service into the class constructor

When you’ve done this, you’ll be able to use the geolocation service from within your home-list component.

9.3.3. Using the geolocation service from the home-list component

The home-list component now has access to the geolocation service, so use it! Remember, your getPosition method in the service accepts three callback functions, so you’ll need to create those functions before you can call the method.

As the geolocation process can take a few seconds before you even start searching the database for locations, you’ll also want to provide some useful messages to users so that they know what’s going on.

You already have an element for messages in your HTML, but it’s currently in homepage.component.html, and you need it in home-list.component.html. Find the <div class="error"></div> in the homepage HTML and remove it. Then, paste it into the top of home-list.component.html, adding a binding so that you can display messages like so:

<div class="error">{{message}}</div>
<div class="card" *ngFor="let location of locations">

With this code, you’ll be able to use the message binding to keep the user up to date on what’s happening. Now you’re ready to create the callback functions.

Creating the geolocation callback functions

Inside the component, create three new private members, one for each of the possible geolocation outcomes:

  • Successful geolocation attempt
  • Unsuccessful geolocation attempt
  • Geolocation not supported

You’ll also update the messages being displayed to users, letting them know that the system is doing something. This message is particularly important, because geolocation can take a second or two.

The success callback is the existing getLocations method, with some additional message-setting thrown in: the other two set error messages, as shown in the following listing. As you’ll be using the message binding from within these new functions, you’ll also need to define it as a property of the class with type string.

Listing 9.16. Setting up the geolocation callback functions in home-list.component.ts
export class HomeListComponent implements OnInit {

  constructor(
    private loc8rDataService: Loc8rDataService,
    private geolocationService: GeolocationService
  ) { }

  public locations: Location[];

  public message: string;                                         1

  private getLocations(position: any): void {
    this.message = 'Searching for nearby places';                 2
    this.loc8rDataService
      .getLocations()
      .then(foundLocations => {
        this.message = foundLocations.length > 0 ? '' :
        'No locations found';                                   2
        this.locations = foundLocations;
      });
  }

  private showError(error: any): void {                           3
    this.message = error.message;                                 3
  };                                                              3

  private noGeo(): void {                                         4
    this.message = 'Geolocation not supported by this browser.';  4
  };                                                              4

  ngOnInit() {
    this.getLocations();
  }

}

  • 1 Defines the message property of type string
  • 2 Sets some messages inside the existing getLocations member
  • 3 The function to run if geolocation is supported but not successful
  • 4 The function to run if geolocation isn’t supported by browser

You’ve got your three callback functions there for success, failure, and error. Now you need to use your geolocation service rather than call getLocations() on the ngOnInit() of the component.

Calling the geolocation service

To call the getPosition method of your geolocation service, you’ll need to create a new member in the home-list component and call it on init instead of calling the getLocations method directly.

Your geolocation service accepts three callback parameters—success, error, and unsupported—so you can add a new member to home-list.component.ts called getPosition that calls your service, passing through your callback functions. That member should look like this:

private getPosition(): void {
  this.message = 'Getting your location...';
  this.geolocationService.getPosition(
    this.getLocations,
    this.showError,
    this.noGeo);
}

Then, you need to call this member when the component is initialized, instead of the getLocations method, so replace the call in ngOnInit to be this new member:

ngOnInit() {
  this.getPosition();
}

Save this code, and head to the browser. You should see something like figure 9.10, where the browser asks you for permission to access your location.

Figure 9.10. A successful call to your geolocation service is marked by a browser request to know your location.

Great news—until you click Allow and the screen hangs on the Getting your location message, quietly throwing a JavaScript error in the background. The error you’re getting says Cannot set property 'message' of null and looks like figure 9.11.

Figure 9.11. Error message shown when you’re trying to set messages in the geolocation callback

This message tells you what the problem is and where it occurs, which helps you fix it.

Working with this in callbacks across components and services

You can see from the error in figure 9.11 that it can’t set this.message inside the getLocations callback, because this is null. When passing the class member through as a callback, you lose the context of this, which is the instance of the class itself.

Luckily, the fix is easy. You can send the context through by binding this to each callback as you send it. Where each callback function is passed, add .bind(this) to the end.

Listing 9.17. Binding this to geolocation callback functions in home-list.component.ts
private getPosition(): void {
  this.message = 'Getting your location...';
  this.geolocationService.getPosition(
    this.getLocations.bind(this),
    this.showError.bind(this),
    this.noGeo.bind(this)
  );
}

Now you’re binding the context of this to the callback function so that it exists when you need it. When you visit the browser again, you have success! After displaying a few messages and getting your location, the browser displays home-list again.

But you’re not using the location yet. You’re getting it but doing nothing with it. You’ll change that situation next.

Using the geolocation coordinates to query the API

In home-list.component.ts, the getPosition method calls your geolocation service to get the coordinates. When it’s successful, it calls the getLocations method—again in home-list.component.ts—as a callback, passing the position as a parameter. You need to update this callback to receive the position. Then this callback calls your data service to search for locations. You need to pass the coordinates to the service, and then update the service to use these values when calling the API.

You have two things to update. Starting with getLocations() in home-list.component.ts, you need to update it to accept a position parameter, extract the coordinates from it, and pass them through to the data service, as highlighted in the following listing.

Listing 9.18. Updating home-list.component.ts to use the geolocation position
private getLocations(position: any): void {           1
  this.message = 'Searching for nearby places';
  const lat: number = position.coords.latitude;       2
  const lng: number = position.coords.longitude;      2
  this.loc8rDataService
    .getLocations(lat, lng)                           3
    .then(foundLocations => {
      this.message = foundLocations.length > 0 ? '' : 'No locations found';
      this.locations = foundLocations;
    });
}

  • 1 Accepts the position as a parameter
  • 2 Extracts the latitude and longitude coordinates from the position
  • 3 Passes the coordinates to the data service call

You’re now getting the position from the geolocation service, extracting the latitude and longitude coordinates, and passing them to the data service. To get the last piece in place, you need to update the data service to accept the coordinate parameters and use them instead of the hardcoded values.

Listing 9.19. Updating loc8r-data.service.ts to use the geolocation coordinates
public getLocations(lat: number, lng: number): Promise<Location[]> {       1
  const maxDistance: number = 20000;                                       2
  const url: string = `${this.apiBaseUrl}/locations?lng=${lng}&lat=${lat}&
  maxDistance=${maxDistance}`;
  return this.http
    .get(url)
    .toPromise()
    .then(response => response.json() as Location[])
    .catch(this.handleError);
}

  • 1 Accepts lat and lng parameters of type number
  • 2 Deletes the hardcoded values you had for lat and lng before

Now the coordinates are finding their way from the geolocation service to the API call, so you’re now using Loc8r to find places near you! If you check it out in the browser—if you’ve added some places within 20 km of where you are—you should see them listed, as shown in figure 9.12. You’ll probably notice a slight change in the distance coordinates, depending on how accurate your test data was.

Figure 9.12. The Loc8r homepage as an Angular app, using geolocation to find places nearby from your own API

That’s the last piece of the puzzle for the homepage. Loc8r now finds your current location and lists the places near you, which was the whole idea from the start. The last thing you’ll do in this chapter is sort out the About page, during which you’ll explore the challenges of injecting HTML through Angular bindings.

9.4. Safely binding HTML content

The current status of the About page in the Angular SPA is that it exists only as a default skeleton page, as you created it to demonstrate navigation and routing in Angular. In this section, you’ll complete the page.

9.4.1. Adding the About page content to the app

The About page should be fairly straightforward. You add the content to the component definition and create the simple markup with the bindings to display it. Easy, right?

Start by adding the content to the component definition. In the following listing, you can see the class definition in about.component.ts. You’re defining a pageContent member to hold all the text information, as you’ve done before. We’ve trimmed the text in the main content area to save ink and trees.

Listing 9.20. Creating the Angular controller for the About page
export class AboutComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

  public pageContent = {
    header : {
      title : 'About Loc8r',
      strapline : ''
    },
    content : 'Loc8r was created to help people find places to sit
    down and get a bit of work done.

Lorem ipsum dolor sit
    amet, consectetur adipiscing elit.'
  };
}

As components go, this one is simple. No magic is going on here. Note, though, that you’ve still got the characters to denote line breaks.

Next, you need to create the HTML layout. From your original Pug templates, you know what the markup needs to be; you need a page header and then a couple of <div>s to hold the content. For the page header, you can reuse the pageHeader component that you created earlier and pass the data through as you did for the homepage. There’s not much to the rest of the markup. The entire contents of about.component.html are shown in the following snippet:

<app-page-header [content]="pageContent.header"></app-page-header>
<div class="row">
  <div class="col-12 col-lg-8">{{ pageContent.content }}</div>
</div>

Again, nothing unusual here—only the page header, some HTML, and a standard Angular binding. If you look at this page in the browser, you’ll see that the content is coming through, but the line breaks aren’t displaying, as illustrated in figure 9.13.

Figure 9.13. The content for the About page is coming through from the controller, but the line breaks are being ignored.

This situation isn’t ideal. You want your text to be readable and shown as originally intended. If you can change the way that the distances appear on the homepage by using a pipe, why not do the same thing to fix the line breaks? Give it a shot, and create a new pipe.

9.4.2. Creating a pipe to transform the line breaks

You want to create a pipe that takes the provided text and replaces each instance of with a <br/> tag. You’ve already solved this problem in Pug by using a JavaScript replace command, as shown in the following code snippet:

p !{(content).replace(/
/g, '<br/>')}

With Angular, you can’t do this inline. Instead, you need to create a pipe and apply it to the binding.

Creating an htmlLineBreaks pipe

As you’ve already seen, pipes are best created by the Angular CLI, so run the following command in terminal to generate the files and register the pipe with the application:

$ ng generate pipe html-line-breaks

The pipe itself is fairly straightforward. It needs to accept incoming text as a string value. Replace each with a <br/>, and then return a string value. Update the main content of html-line-breaks.html to look like the following snippet:

export class HtmlLineBreaksPipe implements PipeTransform {

  transform(text: string): string {
    return text.replace(/
/g, '<br/>');
  }

}

When you’ve done that, try using it.

Applying the pipe to the binding

Applying a pipe to a binding is simple; you’ve already done it a few times. In the HTML, add the pipe character (|) after the data object being bound, and follow it with the name of the filter like this:

<div class="col-12 col-lg-8">{{ pageContent.content | htmlLineBreaks }}</div>

Simple, right? But if you try it in the browser, all isn’t quite as you’d hoped. As you can see in figure 9.14, the line breaks are being replaced by <br/>, but they’re being displayed as text instead of rendering as HTML.

Figure 9.14. The <br/> tags being inserted with your filter are being rendered as text rather than HTML tags.

Hmmmm, this isn’t quite what you wanted, but at least the pipe seems to be working. There’s a good reason for this output: security. Angular protects you and your application from malicious attacks by preventing HTML from being injected into a data binding. Think about when you let visitors write reviews for locations, for example. If they could add any HTML they wanted to, someone could easily insert a <script> tag and run some JavaScript, hijacking the page.

But there’s a way to let a subset of HTML tags through into a binding, which you’ll look at next.

9.4.3. Safely binding HTML by using a property binding

Angular lets you pass through some HTML tags if you use a property binding instead of the default bindings you normally use for content. This technique works only for a subset of HTML tags to prevent XSS hacks, attacks, and weaknesses. Think of property binding as being “one-way” binding. The component can’t read the data back out and use it, but it can update it and change the data in the binding.

You used property bindings when you passed data into nested components. Remember building the About page? There, you were binding data to a property you defined in the nested component, which you called content. Here, you’re binding to a native property of a tag—in this case, innerHTML.

Property bindings are denoted by wrapping square brackets around them and then passing the value. You can remove the content binding in about.component.html and use a property binding:

<div class="col-12 col-lg-8" [innerHTML]="pageContent.content |
     htmlLineBreaks"></div>

Note that you can apply pipes to this type of binding too, so you’re still using your htmlLineBreaks pipe. Finally, when you view the About page in the browser, you’ll see the line breaks in place, looking like figure 9.15.

Figure 9.15. Using the htmlLineBreaks pipe in conjunction with the property binding, you now see the line breaks rendering as intended.

Success! You’ve made a great start toward building Loc8r as an Angular SPA. You’ve got a couple of pages, some routing and navigation, geolocation, and a great modular application architecture. Keep on moving!

9.5. Challenge

Use what you’ve learned about Angular so far and create a new component called rating-stars. This component will be used in the homepage listing section and in the other places where you display rating stars, which you’ll be building out in the next section.

This new component should

  • Accept an incoming number value (the rating)
  • Display the correct number of solid stars based on the rating
  • Be reusable many times on a single page

As a clue, your elements should look something like this:

<app-rating-stars [rating]="location.rating"></app-rating-stars>

Good luck! The code (should you need it) is available in GitHub, on the chapter-09 branch.

In chapter 10, you’ll continue building out the Angular SPA, encountering more-complex page layouts and modal popups, and accepting user input via forms.

Summary

In this chapter, you learned

  • That Angular has a Router and how it works
  • How to build a functional website and use site navigation
  • That using nested components to create a modular and scalable application is best practice
  • How to work with external interfaces like the browser’s geolocation capabilities
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.34.87