Chapter 10. Building an SPA with Angular: The next level

This chapter covers

  • Making pretty URLs
  • Adding multiple views to an SPA
  • Going from one page to another without reloading the application
  • Using AngularUI to get Twitter Bootstrap components as preconfigured Angular directives

In this chapter we’re continuing on with the work we started in chapter 9 by building a single-page application. By the end of this chapter the Loc8r application will be a single Angular application that uses our API to get the data.

Figure 10.1 shows where we’re at in the overall plan, still re-creating the main application as an Angular SPA.

Figure 10.1. This chapter continues the work we started in chapter 9 of re-creating the Loc8r application as an Angular SPA, moving the application logic from the back end to the front end.

We’ll start off by decoupling the Angular application from the server-side application—it’s still being incorporated into a Jade template. As part of this we’ll see how to make pretty URLs, removing the #. When this is done we’ll create the missing pages and functionality and see how to inject HTML into a binding, use URL parameters in routes, and use prebuilt directives based on Twitter Bootstrap components. Along the way we’ll keep an eye on best practices, of course.

10.1. A full SPA: Removing reliance on the server-side application

As our application stands right now, the navigation, page framework, header, and footer are all held in a Jade template. To use this template we have a controller in app_server. This works okay and might be just right for some scenarios. But to have a real SPA we want everything to do with the client-side application in app_client. The theory here is that the entire SPA could easily be moved and hosted anywhere, if you wanted to take it out of the encapsulating Express application, to a CDN for example.

To achieve this we’ll start by creating the host HTML page in app_client and updating the Express routing to point to this. Following on from this we’ll take the sections of the HTML page and make them into reusable components as directives. Finally we’ll look at a way of making pretty URLs by removing the #.

10.1.1. Creating an isolated HTML host page

Okay, so the first step here is to create the host HTML page in a way that doesn’t rely on the server application routes and controllers.

Create a new index.html

The HTML we want to start with is the same as that already being generated by the layout.jade file. If we grab that and convert it to HTML it will look like the following listing. Save this file in app_client/index.html.

Listing 10.1. Host page converted into HTML
<!DOCTYPE html>
<html ng-app="loc8rApp">
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Loc8r</title>
    <link rel="stylesheet" href="/bootstrap/css/amelia.bootstrap.css">
    <link rel="stylesheet" href="/stylesheets/style.css">
  </head>
  <body>
    <div class="navbar navbar-default navbar-fixed-top">
      <div class="container">
        <div class="navbar-header"><a href="/" class="navbar-brand">Loc8r</a>
          <button type="button" data-toggle="collapse" data-target="#navbar-main" class="navbar-toggle"><span class="icon-bar"></span><span class="icon-bar"></span><span class="icon-bar"></span></button>
        </div>
        <div id="navbar-main" class="navbar-collapse collapse">
          <ul class="nav navbar-nav">
            <li><a href="/#about">About</a></li>
          </ul>
        </div>
      </div>
    </div>
    <div class="container">
      <div ng-view>
      </div>
      <footer>
        <div class="row">
          <div class="col-xs-12"><small>&copy; Simon Holmes 2014</small></div>
        </div>
      </footer>
    </div>
    <script src="/angular/angular.min.js"></script>
    <script src="/lib/angular-route.min.js"></script>
    <script src="/lib/angular-sanitize.min.js"></script>
    <script src="/angular/loc8r.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
    <script src="/bootstrap/js/bootstrap.min.js"></script>
    <script src="/javascript/validation.js"></script>
  </body>
</html>

Nothing magical here, so let’s move on and update the application to actually use this.

Routing to the static HTML file from Express

Let’s take a moment to think about when we want to send this index.html file to the browser. Definitely when someone visits the homepage, that’s given. But if we can get rid of the # from the URLs, which we’re going to look at in a couple of pages, we’ll want to show it for all manner of URLs.

As we’re using Angular to do the routing, we don’t want to double up on that and also manage the routes in Express too. But we don’t want to return this HTML file for all of the requests, because we’re also serving the API requests from this application, and delivering static resources (such as CSS, Javascript, and images). So what shall we do?

You may remember that Express routing stops at the first match, after all middleware is applied. If no route match is found it continues through. We can use this feature to our advantage here. By disabling all of the routes for app_server, no requests to those URLs will match and so they’ll fall through to the end.

At the end we can capture all unmatched URL requests and send our new HTML file. The following code snippet shows the changes we need to make to app.js to make this happen, including commenting out the original routes:

If you restart the application and head to the homepage you’ll see that it works just as it did before, but now we’ve removed the reliance on the server application routes and controllers to deliver the base HTML page.

Tip

When using this approach all unmatched URLs will respond by sending the HTML file that loads the Angular application. So your Angular routing should deal with unknown requests in a suitable manner.

Now let’s make all of that HTML part of the Angular application.

10.1.2. Making reusable page framework directives

So we’re now sending a basic HTML page to deliver our application, but this page has quite a lot of markup in it. This markup would be better off inside the application so that we can work with it more easily in Angular. Remember that with Angular you want to build the DOM, rather than manipulate it afterwards as you would with jQuery.

From the HTML page we’ll take the footer and navigation and turn them into directives, so that we can include them on any page we want to. We’ll do the same for the page header, which is currently in the homepage view.

You may remember from chapter 8 that a directive is comprised of two main parts. Each directive will have a JavaScript file to define it and a view template to display it. In turn, each JavaScript file will have to be added to app.js so that the application can use it, and each directive will be placed as an element (or an attribute of an element) into host views where required.

Making a Footer directive

The footer is the most basic component we have because it requires just a small amount of HTML. There’s a slight catch. If you want to use a directive as an element, you can’t give it the name of an existing tag. So we can’t call the footer directive footer and try to include it in the site as <footer> because the HTML specification already contains a footer tag.

So we’ll call our footer footerGeneric and create a folder in app_client/common/directives called footerGeneric. In this folder we’ll put both the HTML and JavaScript files required for the directive.

Starting off with the HTML we can create a file called footerGeneric.template.html and paste in the HTML for a footer, as shown in the following code snippet:

<footer>
  <div class="row">
    <div class="col-xs-12"><small>&copy; Simon Holmes 2014</small></div>
  </div>
</footer>

Next we need to create the associated JavaScript file as footerGeneric.directive.js. In the following listing we use this file to define the new directive, register it with the main application, and assign the HTML file we’ve just created as the view template.

Listing 10.2. Defining the generic footer as a directive: footerGeneric.directive.js
(function () {

  angular
    .module('loc8rApp')
    .directive('footerGeneric', footerGeneric);
  function footerGeneric () {
    return {
      restrict: 'EA',
      templateUrl: '/common/directives/footerGeneric/
      footerGeneric.template.html'
    };
   }

})();

When that file is in place and saved, remember to add it to the appClientFiles array in app.js. Now when we want to include a footer in one of our Angular pages we can use the new element <footer-generic></footer-generic>.

Moving the navigation into a directive

The navigation directive is very similar in approach to the footer. It contains more HTML but doesn’t need to do anything clever with the data. So we’ll just create another folder called navigation in the same place as the footer directive folder, app_client/common/directives.

This folder will host the HTML and JavaScript files again. The following listing shows the HTML we need for the navigation template.

Listing 10.3. Navigation HTML: navigation.template.html
<div class="navbar navbar-default navbar-fixed-top">
  <div class="container">
    <div class="navbar-header"><a href="/" class="navbar-brand">Loc8r</a>
      <button type="button" data-toggle="collapse" data-target="#navbar-main" class="navbar-toggle"><span class="icon-bar"></span><span class="icon-bar"></span><span class="icon-bar"></span></button>
    </div>
    <div id="navbar-main" class="navbar-collapse collapse">
      <ul class="nav navbar-nav">
        <li><a href="/about">About</a></li>
      </ul>
    </div>
  </div>
</div>

The next listing shows the JavaScript definition for the navigation directive.

Listing 10.4. Defining the navigation directive: navigation.directive.js
(function () {

  angular
    .module('loc8rApp')
    .directive('navigation', navigation);

  function navigation () {
    return {
      restrict: 'EA',
      templateUrl: '/common/directives/navigation/navigation.template.html'
    };
  }

})();

Don’t forget to add the JavaScript file to the array in app.js! The main reasons for creating these separate files and folders are maintainability and reusability. If each file and folder does one thing, it’s easier to know where to go to fix or update something. It’s also easier to take a component from one project to another.

Creating a directive for the page header

The page header is slightly different as a directive. It needs to display different data on different pages. Like we did with the rating-stars directive we’ll create an isolate scope in Angular and pass the data through.

As before, create a new folder called pageHeader and create the empty HTML and JavaScript files we’ll need. We’ll start with the HTML that is in the following code snippet. We can lift this directly from the homepage view template, but we need to change the data binding. As we’re using an isolate scope we won’t have direct access to the data in vm. Instead, we’ll say that we want the data for the title and strapline to be held in an object called content. For example

Next up we define the directive as we’ve done with the others, this time adding the scope option back in as shown in listing 10.5. We’ll use the scope option to pass through the content object that the HTML expects. To pass the content object through, this directive expects to receive it from the binding when it’s used.

Listing 10.5. Defining the page header directive: pageHeader.directive.js

When we use this directive in a controller view we’ll need to pass through the content object as an attribute of the element. For example, we’ll be using it like this:

<page-header content="vm.pageHeader"></page-header>

Using an isolate scope like this protects the directive from any changes in scope names. So long as you pass it the content it expects it doesn’t care what you called it before. Once again this makes code really reusable.

Again, don’t forget to add this file to the appClientFiles array in the Express app.js file.

Final homepage template

Now that we’ve created all of the directives we can add them to our homepage view template as shown in the following listing. The new directives are shown in bold, but to keep the DOM structure intact we’ve also had to add a little bit of the container markup from the index.html file.

Listing 10.6. Complete homepage view template
<navigation></navigation>

<div class="container">
  <page-header content="vm.pageHeader"></page-header>

  <div class="row">
    <div class="col-xs-12 col-sm-8">
      <label for="filter">Filter results</label>
      <input id="filter" type="text", name="filter", ng-model="textFilter">
      <div class="error">{{ vm.message }}</div>
      <div class="row list-group">
        <div class="col-xs-12 list-group-item" ng-repeat="location in vm.data.locations | filter : textFilter">
          <h4>
            <a href="#/location/{{ location._id }}">{{ location.name }}</a>
            <small class="rating-stars" rating-stars rating="location.rating"></small>
            <span class="badge pull-right badge-default">{{ location.distance | formatDistance }}</span>
          </h4>
          <p class="address">{{ location.address }}</p>
          <p>
            <span class="label label-warning label-facility" ng-repeat="facility in location.facilities">
              {{ facility }}
            </span>
          </p>
        </div>
      </div>
    </div>
    <div class="col-xs-12 col-sm-4">
      <p class="lead">{{ vm.sidebar.content }}</p>
    </div>
 </div>

  <footer-generic></footer-generic>
</div>

Using directives like this—so long as they’re named well—makes it really easy to understand the structure of your view at a glance, without getting bogged down in loads of markup.

We’ve taken all the markup from the index.html file, so what does that look like now?

Final index.html file

Having taken all of the HTML markup and moved it into directives and views, there’s not much left in the index.html file. But that’s exactly what we wanted. As the controllers and views are now managing the entire content of a page we need to move the ng-view directive into the body tag.

We can see this in action in the following listing, with the new minimal index.html file.

Listing 10.7. Final index.html file

Now we really do have an SPA. We have a single minimal HTML file, with everything else being managed by Angular. Figure 10.2 shows the browser view and the HTML source.

Figure 10.2. Now a real SPA, the source of the HTML page is minimal but the application is still fully functional.

Next we’re going to look at an option for removing the # from URLs.

10.1.3. Removing the # from URLs

A common request for SPAs is to have pretty URLs. Our URLs aren’t bad, but they do all have that # in them. Angular provides a method of removing them from the address bar, but please note that this doesn’t work well with Internet Explorer 9 or below. If you need to support early versions of Internet Explorer, don’t use this part.

The HTML5 spec allows browsers to push states into the navigation history. The main reason for this is to give browsers a way to use the back button in an SPA, preventing the back button from taking them straight away from the site they’re looking at.

Angular routing can make use of this. We just have to switch it on!

Using $locationProvider and the HTML5 mode

To make use of this HTML5 mode we need to add a new provider to the Angular application configuration. We’re already using $routeProvider, and now we need to pass in $locationProvider. This is native to Angular so we don’t have to download any additional libraries.

Enabling the HTML5 mode will be a simple one-liner, like this:

$locationProvider.html5Mode(true);

In the following listing we make some changes to app_client/app.js to update the template URLs, pass $locationProvider into config, and set the html5Mode to be true.

Listing 10.8. Enabling the HTML5 history API

Now when you reload the application, in a modern browser, the URL for the home-page will no longer end in #/. Instead, it will be nice and clean on your domain. If you load it in IE9, the page will still work, but it will have the #.

Working with Internet Explorer

This type of routing won’t work in IE8 or 9 as they don’t have access to the HTML5 API. The application will still be usable in these browsers and Angular will fall back to using the #. So navigating through the application works just fine, albeit with the /#/ element in the URL.

The problem arises if someone copies and pastes a deep link without a hash and tries to use it in IE9. Internet Explorer will just render the homepage, as that’s the way our default routing is set up.

Now, you didn’t hear this from me, but here’s a nasty little fix that you can put right at the top of the homepage controller if you want to. When the homepage controller runs it will check to see what the path name of the URL is. If it’s not for the homepage—that is, just a /—it will take the path name, prefix it with a #, and redirect the page, as follows:

if (window.location.pathname !== '/') {
  window.location.href = '/#' + window.location.pathname;
}

I did say it was nasty. If someone pastes a URL without a # into IE9 they’ll get a flicker as the homepage starts to load before Angular redirects to the correct route. It’s not ideal, so think hard before going down this route if you need to support older versions of Internet Explorer.

Right, that’s enough of nasty hacking! Let’s get back to it and add another page to our application.

10.2 Adding additional pages and dynamically injecting HTML

The concept of an SPA is that the server delivers one page to the browser, and the client-side application does everything else. In this section we’re going to see how to include additional pages by adding the About page. We’ll also deal with an issue that arises when you try to inject HTML into an Angular binding.

10.2.1. Adding a new route and page to the SPA

You’ve probably got an idea of how this is going to work, using the config in app.js to add the route, pointing to a new template and controller. If that is what you were thinking then you were spot on!

Updating the navigation link

The first step we need to take is to update the About entry in the main navigation. It currently points to /about, but that’s not good for the Angular routing. Even though we’re using the HTML5 mode and you don’t see the # in the URL, the paths all need to come after a #. So all we need to do is update navigation.template.html, inserting a # into the About link as shown in the following code snippet:

Well, that was an easy first step. Next we’ll jump into Angular and add the routing definition.

Adding the route definition

Now we need to add a route to the Angular $routeProvider configuration in app_client/app.js. To do this we can duplicate the entry for the homepage and change the path, template URL, and controller name.

Just like we did when creating the About page on the server side we’ll define a reusable generic view for a simple page of text. In the following listing we can see the new route added to config making all of the required changes.

Listing 10.9. Add a new Angular route definition for the About page

Seeing this in place gives us a good idea of what we need to do next. This route contains a controller and a view template that don’t exist yet. Let’s deal with the controller first.

Creating the controller

This is going to be a new controller, so it needs a new file. So in app_client create a folder called about, and in there create a new file called about.controller.js. This will hold the controller for the About page.

In this file we’ll create an IIFE to protect the scopes, attach the controller to the loc8rApp application, and, of course, define the controller. The controller in this case is fairly simple—we just need to set the page title and the page content. For the content we’ll take what we’re using in Express. This is in the about export in app_server/controllers/main.js. Your text should have some line breaks in as ; you’ll need them in to follow through the rest of this section.

In the following listing we can see the complete code for the about.controller.js file. I’ve trimmed down the text in the main content area to save ink and trees.

Listing 10.10. Creating the Angular controller for the About page
(function () {

  angular
    .module('loc8rApp')
    .controller('aboutCtrl', aboutCtrl);

  function aboutCtrl() {
    var vm = this;

    vm.pageHeader = {
      title: 'About Loc8r',
    };
    vm.main = {
       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 controllers go this is pretty simple. No magic going on here; we’re just using the vm variable to hold the view model data like we did with the homepage controller. We, of course, need to make sure the application knows about this file. The following code snippet shows us adding it to the appClientFiles array in the main app.js file in Express:

var appClientFiles = [
  'app_client/app.js',
  'app_client/home/home.controller.js',
  'app_client/about/about.controller.js',

  'app_client/common/services/geolocation.service.js',
  'app_client/common/services/loc8rData.service.js',
  'app_client/common/filters/formatDistance.filter.js',
  'app_client/common/directives/ratingStars/ratingStars.directive.js'
];

When the Node application restarts our new file will be added to the single minified file we’re now using. But if we try to view the page now it will just be empty, as we haven’t created the view template yet.

Creating the new common template

For this new generic text template we’ve already defined in the route config where the file will be: /app_client/common/views/genericText.view.html. So go ahead and create this file. From our original Jade templates we also know what the markup needs to be. Converting the Jade to an Angular template and adding in our layout directives gives us the following:

<navigation></navigation>

<div class="container">
  <page-header content="vm.pageHeader"></page-header>

  <div class="row">
    <div class="col-md-6 col-sm-12">
      <p>{{ vm.main.content }}/p>
    </div>
  </div>

  <footer-generic></footer-generic>
</div>

Again, nothing unusual here. Just some HTML and standard Angular bindings. If we take a look at this page in the browser we’ll see that the content is coming through, but the line breaks aren’t displaying, as illustrated in figure 10.3.

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

This isn’t ideal. We want our text to be readable, and shown as originally intended. If we can change the way the distances appear on the homepage using a filter, why not do the same thing to fix the line breaks? Let’s give it a shot and create a new filter.

10.2.2. Creating a filter to transform the line breaks

So, we want to create a filter that will take the provided text and replace each instance of with a <br/> tag. We’ve actually already solved this problem in Jade, using a JavaScript replace command as shown in the following code snippet:

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

With Angular we can’t do this inline; instead we need to create a filter and apply it to the binding.

Creating addHtmlLineBreaks filter

This is likely to be a common filter, so we’ll put it in the common filters folder alongside formatDistance.filter.js. Again, the contents of the file need to be wrapped in an IIFE, and the filter needs to be registered with the application.

The filter itself is fairly straightforward, returning a function that accepts incoming text and replaces each with a <br/>. Create a new file called addHtmlLineBreaks .filter.js and enter the contents shown in the following code snippet:

(function () {

  angular
    .module('loc8rApp')
    .filter('addHtmlLineBreaks', addHtmlLineBreaks);

  function addHtmlLineBreaks () {
    return function (text) {
      var output = text.replace(/
/g, '<br/>');
      return output;
    };
  }

})();

Before you can do anything with this new filter remember to add it to the appClientFiles array in the Express app.js. When you’ve done that, let’s try using it.

Applying the filter to the binding

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

<p>{{ vm.main.content | addHtmlLineBreaks }}</p>

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

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

Hmmmm. Not quite what we wanted, but at least the filter seems to be working! There’s a very 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 we let visitors write reviews for locations, for example. If they could put any HTML in that they wanted, 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 we’ll look at now.

10.2.3. Sending HTML through an Angular binding

We’re not the first to have a legitimate reason to want to pass some HTML into a binding, so Angular has an answer to this. We can use a service called angular-sanitize, which allows a certain subset of HTML tags to be included in a data binding.

Downloading ng-sanitize

In chapter 9 we downloaded angular-route from code.angularjs.org, and now we need to do the same for angular-sanitize. Find the correct branch again for the release of Angular you’re using (1.2.19 in my case) and download the two minimized angular-sanitize files angular-sanitize.min.js and angular-sanitize.min.js.map. Put these files alongside the angular-route files in /app_client/lib.

When they’re in place, bring them to the browser by adding a reference to the JavaScript file in index.html as shown in the following code snippet:

<script src="/angular/angular.min.js"></script>
<script src="/lib/angular-route.min.js"></script>
<script src="/lib/angular-sanitize.min.js"></script>
<script src="/angular/loc8r.min.js"></script>

Okay, next up we need to tell the application we want to use the service.

Adding ngSanitize as an application dependency

To tell our application that we want to use angular-sanitize we use the same approach that we used for ngRoute and add it as a dependency in the module setter. In this case the name of the service exposed to the application is ngSanitize, so as shown in the following code snippet, add this to the array of dependencies in app_clients/app.js:

angular.module('loc8rApp', ['ngRoute', 'ngSanitize']);

Now that this is available to the application we don’t have to update the controller or the filter. But we don’t necessarily want every single data binding to go through the sanitizer; rather, we want to hand pick which should be allowed to parse some HTML. Angular is looking out for us again here, because to use ngSanitize you have to bind your data to a directive, rather than an inline data binding.

Binding to the HTML element as a directive

So ngSanitize doesn’t just interfere with all data bindings in all templates, it exposes a directive that you bind to. This directive is called ng-bind-html. As with other directives this is added as an attribute of an HTML element, with the binding and filter passed through as the value.

In the following code snippet we can see how to use this, passing in our content data binding and the addHtmlLineBreaks filter. This is in the genericText.view.html file:

<div class="row">
  <div class="col-md-6 col-sm-12">
     <p ng-bind-html="vm.main.content | addHtmlLineBreaks"></p>
  </div>
</div>

This time if you reload the page in the browser you should see the line breaks in place, looking like figure 10.5.

Figure 10.5. Using the addHtmlLineBreaks filter in conjunction with ngSanitize we now see the line breaks rendering as intended.

Great news; it’s always nice to see a win and have a few pieces of a puzzle fall into place. In this little section we’ve seen how to add a new page to an SPA, and also how to inject HTML into an Angular binding. Next we’re going to look at a more interesting page and get the Details page running in our SPA.

10.3. More complex views and routing parameters

In this section we’re going to add the Details page to the Angular SPA. One of the crucial aspects here will be retrieving the location ID from the URL parameter to ensure we get the correct data. Using URL parameters in this way is common practice, and is a very useful technique to know in any framework. We’ll also have to update the data service to hit the API asking for specific location details. As we translate the Jade view into an Angular template we’ll also discover some additional things that Angular does to help us lay out things.

Before we get into the fun stuff we need to get the basic route, controller, and view in place.

10.3.1. Getting the page framework in place

We’ve done this a couple of times now, so we’ll speed through it here. We need to add the route config and create the controller and view files it defines. The controller will also need to add it to application files so that we can use it.

Defining the page route

In app_client/app.js we need to add in the new route. As we want to accept a URL parameter we’ll define the route in the same way we did in Express, by putting a locationid variable at the end of the path, preceded by a semi-colon. The new route in situ is shown in the following listing.

Listing 10.11. Add the Details page route to the Angular application configuration
 function config ($routeProvider, locationProvider) {
   $routeProvider
     .when('/', {
       templateUrl: 'home/home.view.html',
       controller: 'homeCtrl',
       controllerAs: 'vm'
     })
     .when('/about', {
       templateUrl: '/common/views/genericText.view.html',
       controller: 'aboutCtrl',
       controllerAs: 'vm'
     })
      .when('/location/:locationid', {
        templateUrl: '/locationDetail/locationDetail.view.html',
        controller: 'locationDetailCtrl',
        controllerAs: 'vm'
      })
     .otherwise({redirectTo: '/'});
   $locationProvider.html5Mode(true);
 }
Creating the controller file

The Details page template and controller are going to be tightly coupled; they’re not likely to work with any other templates or controllers. Bearing this in mind we’ll put them together in the same folder. Create a new folder called locationDetail in app_client and set up the controller framework shown in the following listing in a new file called locationDetail.controller.js.

Listing 10.12. Controller framework for the Details page
(function () {

  angular
    .module('loc8rApp')
    .controller('locationDetailCtrl', locationDetailCtrl);

  function locationDetailCtrl () {
    var vm = this;

    vm.pageHeader = {
      title: 'Location detail page'
    };
  }

})();

The crucial step that’s easy to forget: add this file to the appClientFiles array in the Express app.js file.

Creating the view template

Inside the same folder as the controller file create the view template file called locationDetail.view.html. For now we’ll just put the standard page framework in here, as you can see in the following code snippet:

<navigation></navigation>

<div class="container">
  <page-header content="vm.pageHeader"></page-header>

  <footer-generic></footer-generic>
</div>
Updating the links in the homepage list

The files are now in place, but we also need to be able to navigate to the Details pages from the homepage listing. Like we did with the About link, we need to add a # to the front of the links in the list so that Angular can access them.

In home.view.html find the line that renders the location name—it’s nested in the <h4> tag. As demonstrated in the following code snippet add a /# to the front of the href:

<h4>
  <a href="/#/location/{{ location._id }}">{{ location.name }}</a>

And there we go, that’s the basics in place. Let’s see about getting that URL parameter and using it to get the correct data.

10.3.2. Using URL parameters in controllers and services

Getting and using a URL parameter is a pretty common requirement, so it’s no major surprise that Angular has a built-in service to help here. That service is called $routeParams and it’s super easy to use.

Using $routeParams to get URL parameters

To use $routeParams in a controller we need to inject it as a dependency and pass it into the function. Once it’s in the controller, $routeParams surfaces as an object holding any URL parameters it has matched. It’s so easy to use; let’s just look at it in code, updating the locationDetailCtrl function as shown in the following code snippet:

See? How easy was that? We can see in the screenshot in figure 10.6 that the location ID is being taken from the URL and output in the page header. This is not where we want to end up, but it’s good to see it working.

Figure 10.6. Using $routeParams we can get the location ID from the URL and use it in a controller, shown here by outputting it in the page header.

Let’s use that location ID to get some data from the API so that we can make the page useful again. To do so we’ll need to create the data service to hit the right API.

Creating the data service to call the API

In the API we built in chapter 6 we created an end point that would accept a location ID and return the associated data. The URL path for this is /api/location/:locationid. To interrogate this URL we’ll add a new method to our loc8rData service.

The following listing shows how simple this is, adding and exposing a new locationById method that accepts a locationid parameter. The method then uses the locationid in the $http call to the API end point.

Listing 10.13. Add a method to the data service to call the API

Using the service to get data

To use the service we need to inject the loc8rData service into the controller. Once it’s there we can follow the pattern we used on the homepage when we hit the API to get the list of locations. As a reminder, the data service using the $http method is asynchronous; upon completion it will invoke either the success or error promise.

In the following listing we inject loc8rData into the locationDetailCtrl function, and call the locationById method passing the location ID as the parameter. On a successful completion of the request we save the returned data to the view model in vm.data.location and display the location name in the page header.

Listing 10.14. Using the service from the controller to get the location data

So that’s pretty powerful and pretty easy. We can see it working in the browser in figure 10.7, outputting a location name in the page header.

Figure 10.7. Proving that we’re getting data from the API by outputting the location name in the page header

That’s another good step complete. Now we need to put the view together.

10.3.3. Building the Details page view

The next step is to rebuild the view. We’ve got a Jade template with Jade data-bindings, and we need to transform this into HTML with Angular bindings. There are quite a few bindings to put in place and some loops using ng-repeat. We’ll also use the rating-stars directive again to show the overall rating and the rating for each review. And we’ll need to allow line breaks in the review text by using the addHtmlLineBreaks filter.

Getting the main template in place

The following listing shows everything in place with the bindings in bold. This code should be added to the locationDetail.view.html file, between the page header and footer. There are some pieces we’ve left out, such as the opening times, which we’ll fill in when we’ve got this in place and tested.

Listing 10.15. Angular view for the Details page

Now that’s quite a long code listing! But that’s to be expected, as there’s quite a lot going on in the Details page. If you look at the page in the browser you’ll see there are a few things that could be fixed. We’re not showing the opening times yet, the reviews are coming through oldest first, and data of the reviews needs formatting.

Adding if-else style logic with ng-switch to show the opening times

It’s not unusual to want some type of if-else logic in a template, to let you show different chunks of HTML depending on a certain parameter. For each opening time we want to display the days in the range, and either a closed message or the opening and closing times. In our Jade template we had a bit of logic, a simple if statement checking whether closed was true, as shown in the following code snippet:

if time.closed
| closed
else
| #{time.opening} - #{time.closing}

We want to do something similar in our Angular view. Instead of using if, Angular works along the line of JavaScript’s switch method, where you define which condition you want to check at the top, and then provide different options depending on the value of the condition.

The key directives here are ng-switch-on for defining the condition to switch on, ng-switch-when for providing a specific value, and ng-switch-default for providing a backup option if none of the specific values matched. We can see all of these in action in the following listing, where we add the opening times to the HTML view.

Listing 10.16. Using ng-switch to display the opening times

And with that we now have a bit of logic in the view. Note that as all of the ng-switch commands are directives they need to be added to HTML tags. Okay, let’s get the reviews showing most recent first.

Changing the display order of a list using the orderBy filter

To help us order things in an ng-repeat list, Angular comes with a native filter called orderBy. Note that orderBy can only be used on arrays, so if you want to sort an object it needs to be converted to an array first.

The orderBy filter can take a couple of arguments. First, you need to state what the list is to be ordered on. This can be a function, but the more common use is to provide a property name of the list being sorted. This is what we’re going to do, using the createdOn property of each review.

The second argument is optional, and defines whether you want to reverse the order or not. This is a Boolean value and defaults to false if left out. We’ll set it to true as we want to show the latest date first.

The following listing shows how we update the view template to add the filter to the ng-repeat directive.

Listing 10.17. Showing the reviews by date using the orderBy filter

And now if you reload the page you should see your reviews showing in the correct order with most recent first. It’s a little hard to tell though, as the date format is not exactly user friendly. Let’s fix it.

Fixing the date format using the date filter

Another filter that comes with Angular is the date filter, which will format a given date in the style that you want. This takes just one argument: the format for your date.

To apply your formatting you send a string describing the output you want. There are too many different options to go into here, but the format is quite easy to get the hang of. To get the format “1 December 2014” we’ll send the string 'd MMMM yyyy' as shown in the following listing.

Listing 10.18. Applying a date filter to format the review dates
<div class="review" ng-repeat="review in vm.data.location.reviews | orderBy:'createdOn':true">
  <div class="well well-sm review-header">
    <span class="rating" rating-stars rating="review.rating"></span>
    <span class="reviewAuthor">{{ review.author }}</span>
    <small class="reviewTimestamp">{{ review.createdOn | date : 'd MMMM yyyy' }}</small>
  </div>
  <div class="col-xs-12">
    <p ng-bind-html="review.reviewText | addHtmlLineBreaks"></p>
  </div>
</div>

And with that we’re done with the layout and formatting of the Details page. The next and final step is to allow reviews to be added, but we’re going to drop the concept of an extra page to do this. Instead we’re going to do it in a modal overlay on the Details page, to provide a slicker experience.

10.4. Using AngularUI components to create a modal popup

In this final section we’re going to see how to add third-party components to an Angular application, and how to post form data. In Loc8r we’ll now enable users to add reviews directly from the Details page, by creating a modal popup that displays when users click the Add Review button. The modal will display the review form, allowing users to input their name, rating, and review. When users submit their reviews we’ll post them to the API so that they’re saved in the database, and add them to the reviews on the page. All of this will happen without leaving the Details page.

The first step then is to create a modal popup to display when somebody clicks the Add Review button.

10.4.1. Getting AngularUI in place

Rather than creating a modal from scratch and figuring out all of the controlling code behind it, we can leverage the hard work of the AngularUI team. They’ve created a number of Bootstrap components written in pure Angular. These components rely only on Angular, no longer jQuery or Bootstrap’s own JavaScript.

Downloading AngularUI

You can get your hands on AngularUI at http://angular-ui.github.io/bootstrap/. You can download the entire library, which defines around 20 components. This is a bit overkill if you just want to use a single component in your application. So you can instead choose to create your own custom build by clicking the Build button. You can then select the components you want—just Modal in our case as shown in figure 10.8—and download the modules.

Figure 10.8. Create a custom build of AngularUI using just the component you need.

Using a custom build like this will dramatically reduce the file size; in this case we’ve gone from 65 kb to 13 kb. Open the zip file you’ve downloaded and copy the two minified JavaScript files to the lib folder in app_client.

Now you can reference them in index.html along with the other library files, before our main application file, as shown in the following code snippet:

<script src="/angular/angular.min.js"></script>
<script src="/lib/angular-route.min.js"></script>
<script src="/lib/angular-sanitize.min.js"></script>
<script src="/lib/ui-bootstrap-custom-0.12.0.min.js"></script>
<script src="/lib/ui-bootstrap-custom-tpls-0.12.0.min.js"></script>
<script src="/angular/loc8r.min.js"></script>

With the files added we can now use them in the application.

Using AngularUI in the application

To use AngularUI components in our application we need to define it as a dependency at the application level. When this is done we need to define the modal component as a dependency of the controller for the page we’ll use it in.

First, to add AngularUI as an application dependency we simply need to add 'ui.bootstrap' to the array of dependencies in app_client/app.js, as shown in the following code snippet:

angular.module('loc8rApp', ['ngRoute', 'ngSanitize', 'ui.bootstrap']);

And now that they’re in the application, we tell the controller that we want to use the modal component by injecting a $modal dependency, as shown in the following code snippet. Remember that we need to pass it as a parameter into the controller function and add it to the $inject array.

locationDetailCtrl.$inject = ['$routeParams', '$modal', 'loc8rData'];
  function locationDetailCtrl ($routeParams, $modal, loc8rData) {

With those two pieces in place we can now carry on and create the modal.

10.4.2. Adding and using a click handler

Reminding ourselves of what we want to achieve, the aim is to pop up a modal dialogue box when a user clicks the Add Review button. So we need to add a click handler to the button, create the corresponding function in the controller, and then look at how to create the modal.

Adding the ng-click handler

To listen for a click that calls a method in our Angular application, rather than use href or onclick we should use Angular’s click handler ng-click. This behaves in a similar way to onclick, but gives access to your view model methods.

In the following code snippet we add an ng-click handler to the Add Review button in locationDetail.view.html that will call a function in our view model called popupReviewForm:

<a ng-click="vm.popupReviewForm()" class="btn btn-default pull-right">Add review</a>

Okay, the next step is to create the popupReviewForm method in the controller.

Adding the method called by the click handler

Creating the method in the controller is a simple case of declaring vm.popupReviewForm as a function. In the following listing we add the new function and set it to fire a simple alert so that we can test that the ng-click and the method are working together as expected.

Listing 10.19. Adding the method to the controller
function locationDetailCtrl ($routeParams, $modal, loc8rData) {
  var vm = this;
  vm.locationid = $routeParams.locationid;

  loc8rData.locationById(vm.locationid)
    .success(function(data) {
      vm.data = { location: data };
      vm.pageHeader = {
        title: vm.data.location.name
      };
    })
    .error(function (e) {
      console.log(e);
    });

   vm.popupReviewForm = function () {
     alert("Let's add a review!");
   };
}

So if we make these changes and navigate to a location Details page on our site we should see an alert when we click the Add Review button. Figure 10.9 shows this in action, proving that we’ve linked the button and the method correctly.

Figure 10.9. Testing that the Add Review click handler works by using a simple Alert box

10.4.3. Creating a Bootstrap modal with AngularUI

Now it’s time to create the modal dialogue box. Even though we’re not doing it here, it’s quite conceivable that you’d have several actions on a page that could fire a modal window. To prevent any cross-contamination of code each modal is created as a new instance, with its own template and controller.

So we’re going to look at how to define an instance, and then add the view and controller.

Defining an AngularUI modal instance

We’re going to use the popupReviewForm handler from the previous section to define a new modal instance. We’ll assign a template URL and a controller against this just like we do for a directive or route definition.

The syntax for this is the following:

vm.popupReviewForm = function () {
 var modalInstance = $modal.open({
   templateUrl: '/reviewModal/reviewModal.view.html',
   controller: 'reviewModalCtrl as vm',
 });
};

Note that we’re using a different approach to the controllerAs syntax here. The modal component can use the approach but doesn’t currently support using the controllerAs option to specify it. Instead, we define the view model name inline, just like we would if defining it inside an HTML element.

Adding the modal view

To create the HTML for the review modal we’ll blend a combination of the review form we’ve already got and the template markup for a Bootstrap modal. For good measure we’ll add in some data bindings to the form fields, and define a function we can use to cancel the modal.

If we put all this together we end up with the following listing. Save this as a new file in the location we specified as the template URL in listing 10.19: reviewModal .view.html in app_client/reviewModal/.

Listing 10.20. HTML view for the modal popup

Nothing too complex here, just a lot of markup. To use the view, of course, we’ll need to create the controller.

Creating the modal controller

When we defined the modal in the location view controller we specified a controller name that we’d use: reviewModalCtrl. Now it’s time to create that in a file called reviewModal.controller.js, alongside the view we’ve just created.

We’ll start off with the basic controller construct. A modal controller has a dependency on $modalInstance—created by the AngularUI component—that we’ll inject. $modalInstance has a dismiss method that we can invoke when either the Cancel or Close button is clicked. To do this we’ll create the vm.modal.cancel method we reference in the view, and use it to dismiss the modal. All of this is tied together in the following listing.

Listing 10.21. Starting point for the review modal controller

When that’s in place, remember to add it to the array of scripts to concatenate in the Express app.js file. If you now reload the page and click the Add Review button you should see the modal popup display, as shown in Figure 10.10. Clicking on the Cancel button, or anywhere outside the modal, should dismiss it.

Figure 10.10. The modal popup in action, showing the review form

That’s a great start, but the name of the location doesn’t display in the modal header. Let’s fix that by passing data into the modal.

10.4.4. Passing data into the modal

Passing data from the page controller into the view model of the modal controller is a three-step process:

  1. In the modal definition resolve the variables we want to use.
  2. Inject these as dependencies into the modal controller.
  3. Map them to objects in the modal view model.
Resolving variables in the modal instance definition

The first step in passing data to the modal controller is to get the variables into the modal instance definition. This is done using a resolve option. The resolve option is mapped to an object containing one or more parameters that you want to use in the modal. Each parameter should be mapped to a function that returns a value or an object.

We want to have access to the location ID and the location name in our modal, so we’ll resolve a parameter called locationData, and have it return an object containing both the location ID and the name. The following listing shows the additions we need to make to the modal instance definition.

Listing 10.22. Using resolve to pass variable values into the modal

This will enable the modal controller to use a locationData parameter, if we inject it as a dependency.

Dependency injecting the resolved parameters and adding to the view model

For the modal controller to use the parameter we’ve just created, we need to inject it as a dependency. We’ll do this exactly like we would any other dependency injection, as shown in the following code snippet. We’ll also take this opportunity to save the parameter as a property of the view model.

With that in place, we can now use the values of the locationData parameter in the modal.

Using the data passed through

Now that we have the data available in the modal view model we can use it in a binding in the modal view. The following code snippet shows how we update the modal title to display the location name:

<h4 id="myModalLabel" class="modal-title">Add your review for {{ vm.locationData.locationName }}</h4>

By reloading the page in the browser and clicking the Add Review button again you can see the data in action, as shown in figure 10.11.

Figure 10.11. Displaying the name of the location inside the modal popup

Okay, we’re looking good. The final thing we need to do is hook up the form, so that when it’s submitted a review is added.

10.4.5. Using the form to submit a review

Now is the time to make our review form work and actually add a review to the database when it’s submitted. To get to this end point we have a few steps involved:

  1. Have Angular handle the form when it’s submitted.
  2. Validate the form so that only complete data is accepted.
  3. Add a POST handler to our loc8rData service.
  4. POST the review data to the service.
  5. Push the review into the list in the Details page.
Adding onSubmit form handlers

When working with a form in HTML you’d typically have an action and a method to tell the browser where to send the data and the HTTP request method to use. You might also have an onSubmit event handler if you wanted to do anything with the form data using JavaScript before it was sent.

In an Angular SPA we don’t want the form to submit to a different URL taking us to a new page. We want Angular to handle everything. For this we can use Angular’s ng-submit listener to call a function in the view model. The following code snippet shows how this is used, adding it into the form definition, calling a function in the controller that we’ll write in just a moment:

<form id="addReview" name="addReview" role="form" ng-submit="vm.onSubmit()" class="form-horizontal">

Next we need to create the corresponding onSubmit function inside the review modal controller. To test that it’s working we’ll simply log the form data to the console and then return false to prevent the form from submitting. When we built the view for the form we used a property on the view model for each input. Well we actually went one better and put each item as a child property of vm.formData, which makes it nice and easy to get all of the data together. The following code snippet shows the starting point for the onSubmit function to be added to the review modal controller:

Now that we can capture the form data we’ll add in some validation.

Validating the submitted form data

Before we blindly send every form submission to the API to save to the database, we want to do some quick validation to ensure that all of the fields are filled in. If any of them aren’t filled in we’ll display an error message. Your browser may prevent forms from being submitted with empty required fields; if this is the case for you, temporarily remove the required attribute from the form fields to test the Angular validation.

When a form is submitted we’ll start off by removing any existing error messages before checking whether each data item in the form is truthy. If any return false—that is, have no data—we’ll set a form error message in the view model and return false. If all of the data exists we’ll continue to log it to the console as before.

The following listing shows how we need to change the onSubmit function in the review modal controller to handle this validation piece.

Listing 10.23. Adding some basic data validation to the onSubmit handler

Now that we’re creating an error message we want to show it to users when it’s generated. For this we’ll add a new Bootstrap alert div into the modal view template, and bind the message as the content. We only want to show the div when there’s an error message to display, so we’ll add an Angular directive called ng-show. ng-show accepts an expression as the value, and if it evaluates as truthy it will display the element, otherwise it will hide it.

So for us, we can use it to check whether vm.formError has a value, and only show the alert div if it has. The following code snippet shows the addition we need to make to the review modal view template, adding the alert right near the top of the modal body:

<div class="modal-body">
  <div role="alert" ng-show="vm.formError" class="alert alert-danger">{{ vm.formError }}</div>
 <div class="form-group">

We can see this in action in figure 10.12.

Figure 10.12. When an incomplete form is submitted, an error message is displayed.

Updating the data service to accept new reviews

Before we can use this form to post review data we need to add a method to our data service that talks to the correct API endpoint and can post the data. We’ll call this new method addReviewById and have it accept two parameters: a location ID and the review data.

The contents of the method will be just the same as the others, except we’ll be using post instead of get to call the API. The following listing highlights in bold the changes required to the loc8rData function in loc8rData.service.js.

Listing 10.24. Adding a new addReviewById method to the data service
function loc8rData ($http) {
  var locationByCoords = function (lat, lng) {
    return $http.get('/api/locations?lng=' + lng + '&lat=' + lat + '&maxDistance=20');
  };

  var locationById = function (locationid) {
    return $http.get('/api/locations/' + locationid);
  };

  var addReviewById = function (locationid, data) {
    return $http.post('/api/locations/' + locationid + '/reviews', data);
  };

  return {
    locationByCoords : locationByCoords,
    locationById : locationById,
    addReviewById : addReviewById
  };
}

Brilliant; now we can use this data service from our modal.

Sending the form data to the data service

So we’ve got our form data being posted and we’ve got a data service ready to post it to the API. Let’s hook these two up. We’ll use the data service just like we’ve done before; using this new method is no different. We start off by calling the method, which will resolve to either a success or error promise as the data service is using the asynchronous $http method.

To keep the code tidy we’ll move this functionality into its own function called doAddReview. This is all shown in the following listing, including the important part of injecting the loc8rData service into the controller.

Listing 10.25. Sending complete form data to the data service

And now we can send reviews to the database. Just one last thing to make it slick: when the review is sent we want to close the modal popup and add the review to the list.

Closing the modal and displaying the review

Closing the modal and adding the new review to the list are closely tied together. We could just use the dismiss method as we’ve done for the Close and Cancel buttons, but there’s a better way.

The modal instance has a close method as well as a dismiss method. The close method can actually pass some data back to the parent controller. We can use this to pass the review data from the modal controller up into the location view controller. When a new review is posted to the API, we set it up so that the response to a successful posting would return the review object from the database.

We know that this data will be in the correct format to be displayed in the page so it’s the best source of data to send back to the parent controller. So we need to call the modal close method in the success callback of the addReviewById call. Instead of calling it directly we’ll create a helper method like we did for the Cancel button. It all comes together in the following listing.

Listing 10.26. Passing review data into the modal’s close method

The question now arises: How do we use this data? Good question! The close method returns a promise to the parent controller where we defined the modal instance in the first place. We can hook into this promise, and when it’s resolved simply push the new review into the array of reviews as shown in the following listing.

Listing 10.27. Resolving the modal instance promise to update the review list

As the array of reviews is bound to the view template, Angular will automatically update the list of reviews showing. And because we set it up to order by the newest review first, this review will appear at the top of the list as shown in figure 10.13. How easy is that?

Figure 10.13. Add a review in the modal, and when submitted, the modal closes and the review appears at the top of the list without the page reloading.

And that’s it. Our Angular SPA is complete. So let’s take a look at what we’ve learned.

10.5. Summary

In this chapter we’ve covered

  • Taking the whole application code into the client
  • Making pretty URLs using the HTML5 history API
  • Adding multiple views to the application
  • Safely binding text containing HTML elements
  • Using URL parameters
  • Adding if style logic using ng-switch and ng-show
  • Using prebuilt AngularUI components
  • Posting data to the API using the $http service

Coming up next in the final chapter we are going to see how to manage authenticated sessions, by adding the ability for users to register and log in before leaving reviews.

..................Content has been hidden....................

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