Chapter 8. Adding Angular components to an Express application

This chapter covers

  • Getting to know Angular
  • Adding Angular to an existing page
  • Filtering lists of data
  • Using an API for reading data
  • Some Angular jargon: controllers, scope, filters, directives, services

Here it comes. It’s now time to take a look at the final part in the MEAN stack: Angular! We’re going to look at how to include Angular into the Express application, and develop a couple of components to improve our existing Loc8r application. As we go we’ll discuss some of the semantics and key technical terms found in the Angular world. Unlike the other parts of the stack, Angular is opinionated, meaning that things have to be done in a certain way.

Figure 8.1 shows where we are in the overall plan, adding Angular into the front end of the existing Express application.

Figure 8.1. Chapter 8 focuses on adding Angular to the front end of the existing Express application.

The approach taken in this chapter is what you’d do if you wanted to enhance a page, project, or application with a bit of Angular. Building a full application entirely in Angular is coming up in chapters 9 and 10. We’ll use this chapter to get to know Angular a bit too.

In this chapter we’ll focus on improving the experience of the homepage. We’ll use Angular to load the data into the list of locations, and add the ability for users to search and filter the list. To start off we’ll use static data to get the hang of Angular before pulling data from the API, and using the HTML5 location APIs to get the geographical position of the user.

8.1. Getting Angular up and running

Angular is the second JavaScript framework in the MEAN stack, with Express being the other. Express, as we’ve seen, sits on the server, whereas Angular sits on the client side in the browser. Like Express, Angular helps you separate your concerns, dealing with views, data, and logic in distinct areas. The approach is very much in the MVC style, but Angular has been defined as an MVW framework, where the W stands for “whatever works for you.” Sometimes it might be controllers, or the view-model, or services. It depends on what you’re doing at any given point.

Now that you know a little bit about it, let’s get on with the cool stuff. We’ll start by playing a little with some basics of Angular, coming to terms with the concept of twoway data binding, including views, models, view-models, and controllers.

Angular is a client-side framework, so it doesn’t take much installation. The process is covered in appendix A, but it’s essentially as simple as downloading the latest stable version from http://angularjs.org.

8.1.1. Uncovering two-way data binding

So what does two-way data binding actually mean? Back in chapter 1 we talked briefly about how the data model and the view are bound together in Angular, and that both are live. This means that changes to the view update the model, and changes to the model update the view. Remember that we’re not talking about any type of database here—all of this happens in the browser. Figure 8.2 illustrates this two-way binding.

Figure 8.2. In Angular the view and model are bound together with two-way data binding, all in the browser.

Pictures may paint a thousand words, but examples are better yet. So let’s look at our first bit of Angular coding.

Starting with a page of HTML

Say we have a very simple HTML page, where we have an input box and somewhere to display the input. In the following code snippet we have an input field and an <h1> tag, and we want to take whatever text is entered into the input box and immediately display it after the Hello in the h1:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Angular binding test</title>
</head>
<body>
  <input />
  <h1>Hello </h1>
</body>
</html>

If you’re experienced in JavaScript or jQuery you’re probably thinking about how you’d do this, possibly writing code to bind to keystroke events on the input field and then injecting that into the <h1> tag. It’s doable, and it’s not that hard. But with Angular you can actually do something like this without coding any JavaScript at all!

Making the page an Angular application

To make the page an Angular application we have to include Angular in the page; Angular may be extremely clever but it cannot read your mind! Adding it’s simple; it’s just a single external JavaScript file that you can either download and reference directly or reference a CDN version.

Assuming we download and reference it locally we need to add it to the page as follows:

<script src="angular.min.js"></script>

Just referencing the file isn’t enough; we need to tell Angular that this page is an Angular application. To do this we can add a simple attribute, ng-app to the opening <html> tag, like so:

<html ng-app>

This tells Angular that anything nested inside the <html> tag can be considered part of the application.

Tip

ng-app can be assigned to any element on the page if you want to limit the area that Angular has access to. It’s often put into the <html> tag so that Angular can work anywhere within the page.

Binding input and output

As mentioned, we’re going to take the input from the form and show it in the HTML without writing any JavaScript. It sounds unlikely, but we’re really going to do it. We just need to bind an Angular model to the input and the output, and Angular will do the rest. Both bindings will need to reference the same name, so that Angular knows that they share the same model.

First we bind the input to the model, giving it a name—for example, myInput—like this:

<input ng-model="myInput" />

Next we output the value of the model where we want it in the HTML. Angular uses double curly braces {{ }} to define these bindings. To bind the model variable into the view we simply put the name in between the double curly braces, like this:

<h1>Hello {{ myInput }}</h1>

Putting everything together, we should end up with the following listing.

Listing 8.1. Simple Angular binding, binding a model to input and output
<!DOCTYPE html>
<html ng-app>
<head>
<script src="angular.min.js"></script>
  <meta charset="utf-8">
  <title>Angular binding test</title>
</head>
<body>
  <input ng-model="myInput" />
  <h1>Hello {{ myInput }}</h1>
</body>
</html>

Doesn’t look like much, does it? But run it in a browser and try it out, as demonstrating it in screenshots doesn’t really do it justice. If you run it in a browser and start typing in the input box you should see behavior like that shown in figure 8.3.

Figure 8.3. With simple data binding in Angular, as soon as text is typed into the input box it’s output inside the <h1> tag. This is all achieved without having to write a single line of code.

This works because the Angular model is bound to both the input and the output. There may be two elements on the page, but there’s only one model holding the data.

This example is all very well and good, but chances are that you want to do something more with Angular. And that’s going to involve some coding.

8.1.2. Setting up for greatness (and JavaScript code)

To cover the necessary concepts and jargon for getting started with some Angular coding we’ll go for the simple goal of assigning a default value to the myInput model. To do this we’ll need to define our Angular application as a module, and create a controller to manage the scope. Don’t worry if that didn’t make sense—it will by the time we get to the end of this section.

Creating an Angular application as a module

To create and code an Angular application we need to define a module. A module is defined in JavaScript with a name that we choose, and this name is then referenced by the ng-app attribute in the HTML.

Let’s start with this bit so that it’s done. We simply add the name of the Angular application or module as a value of the ng-app attribute in the <html> tag. In the following code snippet we’re saying that we’ll be using an Angular application called myApp:

<html ng-app="myApp">

In an external JavaScript file we can create the definition for this application using a simple setter statement. We have to define the same name, of course, as follows:

angular.module('myApp', []);

This use of the setter syntax is the best practice for defining a module, but to make sense of a common approach that you’ll likely see in online code samples take a look at the following sidebar.

A common but inferior approach

The best practice for defining a module is to use the setter syntax like this:

But it’s quite common among online code examples to see this being assigned to a variable, like this:

The prevalence of this approach is largely due to the original Angular documentation showing this method, but it’s slowly being changed as the documentation gets updated. It’s better to use the setter syntax for a few reasons. It allows you to use the getter syntax for controllers and more that we’ll see soon, and it makes the code easier to read and reduces the risk of reusing an existing variable name.

Defining a controller

Once an application has been defined it can have controllers attached to it. A controller is defined in JavaScript and attached to a specific HTML element. The controller can then work inside the associated element.

For our example we’ll attach the controller to the body. This is done using the Angular attribute ng-controller, giving it the name of the controller we want to use. The following code snippet provides the controller name myController:

<body ng-controller="myController">

Now it’s the turn of the code. Having defined the application module using the setter syntax, we can use the getter syntax to define a controller. The following code snippet shows how to attach a controller called myController to an Angular module called myApp:

This works well, but it would be better for readability, reusability, and testability to use a named function instead of an anonymous function. In the following code snippet we do just that, creating a new named function myController to hold the controller code:

If you refresh the page at this point the mini-application should still work, taking the contents of the input and displaying it as you type. Next we’ll look at scope to see how to give our model a default value.

Introducing scope

Angular applications have scopes just like JavaScript code does. Similar in concept to the JavaScript global scope, Angular has a rootScope. The rootScope is everything within an application, and is implicitly created with the ng-app directive in the HTML. The model for the first version of our “Hello world!” application was held within this scope.

Nested within the rootScope can be one or more child scopes. A child scope is also implicitly created whenever the ng-controller directive is added to the HTML. When we added the controller to the application we actually moved the model out of the rootScope and into the scope of the controller.

In Angular, scope ties together the view, model, and controller, as they all use the same scope. We’ve witnessed scope working with the model and view, even though we didn’t know it. We can make it more obvious by playing with it in the controller.

The controller function can take a $scope parameter because the scope has already been created by Angular. This must be called $scope because behind the scenes it’s dependent on the Angular provider $scopeProvider. This $scope parameter gives us direct access to the model. So setting a default value is as simple as using the standard JavaScript dot notation for accessing object properties. The following code snippet shows how we can update the controller function to accept the $scope parameter and use it to set a default value:

Just because we’ve assigned it a default value, it doesn’t set that value in stone. Updating the input field will still update the model and display it. Figure 8.4 shows this in action.

Figure 8.4. The controller assigns a default value via the scope, but we can still change that value using the input.

At the start of this section we said that we’d define our Angular application as a module and create a controller to manage the scope. That might not have made sense at the start, but hopefully it does now. These are the foundation building blocks of knowledge for Angular.

So now that we know what we’re doing and where Angular is coming from, let’s start adding some components to the Loc8r application.

8.2. Displaying and filtering the homepage list

In this section we’re going to see how to add an Angular component to an application; in this case we’re going to replace existing functionality. We’re going to change the way the homepage is coded, using Angular to display the list of locations on the homepage rather than having Express deliver the HTML. Throughout the course of this section we’ll cover quite a lot of Angular functionality that will be useful in most other projects, including filtering lists, data format filters, services, dependency injection, and using directives for adding in reusable HTML.

8.2.1. Adding Angular to an Express application

From what we’ve just learned in section 8.1 we know what we’ll need to do to add Angular into the Loc8r application. We’ll need to do the following:

  • Download the Angular library file.
  • Create a new JavaScript file for our code.
  • Include these files in the HTML.
  • Define the Angular application in the HTML.

This shouldn’t take long, so let’s get started.

Adding the JavaScript files to the project

As Angular is a client-side framework we need to ensure that Express sends the Java-Script files to the browser rather than trying to run them. The public folder is already set up to deliver static files, so that’s the perfect place for adding a couple of new JavaScript files.

Inside the public folder create a new folder called Angular. Drop the downloaded minimized Angular library file in here. In the same folder create a new JavaScript file called loc8rApp.js—this will hold our Angular code for this chapter.

While we’re here, let’s create the Angular module setter for our application, which we’ll call loc8rApp. In loc8rApp.js enter the following code snippet and save it:

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

And that will do us for now. Next we need to update the views to include the JavaScript and define the Angular application.

Setting up the HTML

Updating the views is pretty easy. We’ll add everything we need at this stage to the layout.jade file so that any of the pages in the application can use Angular.

First let’s add the two JavaScript file links in with the other external files at the bottom of the Jade file, as shown in the following listing.

Listing 8.2. Adding the Angular library and application code to the HTML
script(src='/angular/angular.min.js')
script(src='/angular/loc8rApp.js')
script(src='/javascripts/jquery-1.11.1.min.js')
script(src='/bootstrap/js/bootstrap.min.js')
script(src='/javascript/validation.js')

Then we just need to define the Angular application in the HTML, which we’ll do in the <html> tag again. The following code snippet shows how to do this in Jade syntax, using the loc8rApp name:

html(ng-app='loc8rApp')

And with that, all pages of Loc8r are primed for some Angular action.

8.2.2. Moving data delivery from Express to Angular

If we’re going to use Angular to display the list of locations, then Angular will need to have the data for that list. We’ll start off validating the approach by using data hard-coded in Angular—much like we did when building the Express application—before eventually pulling it from the database.

To achieve this we need to do three things:

  • Remove the API call from the Express controller for the homepage.
  • Add some hard-coded data into the Angular application scope.
  • Update the view template to bind to the Angular data.

We’ll get going by updating Express.

Removing the homepage API call from Express

As it stands, pretty much everything in the Express controller for the homepage (homelist in app_server/controllers/locations.js) is concerned with hitting the API for the data. We can remove this and just call the renderHomepage function, as shown in the following code snippet:

module.exports.homelist = function(req, res){
  renderHomepage(req, res);
};

The renderHomepage function is also set to deal with incoming data, which is no longer needed. In the following listing we remove any references to responseBody and message.

Listing 8.3. Taking the dynamic content out of the renderHomepage Express function
var renderHomepage = function(req, res){
  res.render('locations-list', {
    title: 'Loc8r - find a place to work with wifi',

    pageHeader: {
      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."
   });
};

That’s all we need to do for the controller function. Now we jump over to the Angular code for a moment to add some data into the scope.

Adding hard-coded data into the Angular scope

We’re going to start off with hard-coding some data into the scope so that we can get the view sorted out. The first step is to create a function for our controller code in loc8rApp.js in /public/angular. We’ll call this locationListCtrl, make sure we pass in the $scope parameter, and assign an array of location objects to a property called data.

The following listing shows how this looks with a couple of locations in it; you’ll probably want to add a few more so that you can test the filtering.

Listing 8.4. Create an Angular controller and some test data in loc8rApp.js

That’s all we need to do in the controller to get the data into the scope so that the view can use it. We’ll update the code soon to get the real data from the database. The last thing to remember is to attach the controller to the Angular application. To do this, we’ll use the module getter syntax at the very bottom of the loc8rApp.js file, after the controller code, as shown in the following code snippet:

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

Now the controller is attached to the application and pushing data into the scope. But the view doesn’t define the Angular controller anywhere so it won’t be used yet, and it’s still using the Express data bindings. So let’s jump back over to the Express application and update the homepage view.

Update the Jade view to bind to the Angular controller

The Jade view is still currently trying to use the Express bindings, and will fail because we’ve deleted all of the code from Express. So we need to add the ng-controller directive to a relevant element, swap out the Jade loop for an Angular loop, and change the Jade data bindings to Angular data bindings.

The trick here is to use an Angular loop. Rather than using a for loop Angular has a directive called ng-repeat that allows us to repeat through the elements of an array. The ng-repeat directive will loop through an array, outputting the HTML and data bindings for each entry in the array. For more information, check out the following sidebar.

Using ng-repeat to loop through data objects

Angular’s ng-repeat directive will loop through a given array, rendering the HTML for each of the pieces of data. For example, let’s start with this simple controller adding an array of data to the scope:

var myController = function($scope) {
  $scope.items = ["one", "two", "three"];
};

You can output the items in the array using the ng-repeat directive like this:

<body ng-controller="myController">
  <ul>
    <li ng-repeat="item in items">{{ item }}</li>
  </ul>
</body>

The resulting output, omitting the attributes that Angular will add to the HTML elements, is this:

<ul>
  <li>one</li>
  <li>two</li>
  <li>three</li>
</ul>

Note that Angular will start the repetition on the element where you put the ng-repeat directive, so you must be careful to put it in the right place. In this example it would be very easy to accidentally put it into the <ul> tag instead of the <li> tag, but this would output the following markup:

<ul>
  <li>one</li>
</ul>

</ul>
  <li>two</li>
</ul>
<ul>
  <li>three</li>
</ul>

This is probably not what you were after. ng-repeat is extremely handy and very powerful, but just be careful you put it in the right place.

Everything we want to do is contained in the main content column of the page, so that’s a good place to define the controller. Inside that controller all of the Jade data bindings and loops need to be swapped out for their Angular counterparts. The following listing runs through the changes made to make this happen.

Listing 8.5. Changing the Jade bindings to Angular bindings

That’s the last piece of the puzzle for our first pass at getting Angular running in the homepage. We’ve added the scripts, defined the application in the HTML and Java-Script, created the controller, and bound the data into the HTML. So now when we reload the homepage, we get something like figure 8.5.

Figure 8.5. The homepage data being supplied and rendered by Angular instead of Express. There are some layout issues that need fixing.

We can see from figure 8.5 that a few things need tweaking. We had been using Express to format the distance before sending it to the view, and had also used a Jade mixin to output rating stars instead of just a number. Angular gives us a couple of tools to achieve these things in a different way: filters and directives. We’ll look at filters first.

8.2.3. Using Angular filters to format data

Filters allow you to specify your chosen format for a given piece of data. Angular has some built-in filters such as formatting a date, currency, and text. You can apply these filters directly within the data binding in the HTML. To apply one insert a pipe | after the value followed by the name of the filter.

The following code snippet shows an example of using the currency filter:

<div>{{ 123.2345 | currency }}</div>
<!-- Output: $123.23 -->

Some of the filters allow you to specify options. The currency filter can be told to output a different currency symbol, as shown in the following code snippet:

<div>{{ 123.2345 + 321.321 | currency:"£" }}</div>
<!-- Output: £444.56 -->

The filters aren’t limited to working with numbers, of course; you can also work with strings and dates. A couple of quick examples are as follows:

The built-in filters that come with Angular are pretty good. While it’s very useful to have these at your disposal you may find you need to do something different, which is the case we have with Loc8r when we want to format the distances. The good news is, you can create your own custom filters for your application.

Creating a custom filter

The Loc8r API returns distances as long numbers such as 0.296456. This distance is actually in kilometers, but doesn’t explicitly state any unit of measurement. We want this to look better to the end user.

We’ve already solved this problem in Express, but now we need to solve it in Angular. As we’re using JavaScript in the back end and the front end we can lift most of the code we need from Express and pop it into Angular.

In Express, in locations.js in app_server/controllers, we had two functions for formatting the data, _isNumeric and _formatDistance. We can keep the logic of these functions intact. The only change we need to make when we move it into the Angular application is to change the formatDistance function to return a function that does the processing, rather than doing the processing itself. The following listing shows the code we need to put into our Loc8rApp.js file.

Listing 8.6. Creating a custom filter to apply formatting to distances

This shows how to create a custom filter, but now we need to add it to the application and use it in the HTML.

Adding and using custom filters

When we have our custom filter built, we want to add it to our application so that we can use it. At the bottom of the loc8rApp.js file we can chain it to the Angular module getter syntax, where we registered the controller for the application. The principle is the same, but rather than defining a controller, this time we’re defining a filter.

The following code snippet shows how this section of the file should now look:

angular
  .module('loc8rApp')
  .controller('locationListCtrl', locationListCtrl)
  .filter('formatDistance', formatDistance);

With this in place the filter is now available to the application, so we can reference it in the code, just like we would with any other filter. In the following code snippet we add the filter to the relevant place in the Jade template:

.col-xs-12.list-group-item(ng-repeat="location in data.locations")
  h4
    a(href="/location/{{ location._id }}") {{ location.name }}
    small {{ location.rating }}
     span.badge.pull-right.badge-default {{ location.distance | formatDistance }}
  p.address {{ location.address }}

This should now be formatting our distance nicely like before, and we should have something that looks like figure 8.6.

Figure 8.6. Custom Angular filter formats and displays the distances in the way we want

And that wraps up a quick tour of filters, taking a look at some of the native Angular filters and how to create your own. Next up on the to-do list is using directives for HTML snippets with the aim of adding back the rating stars.

8.2.4. Using Angular directives to create HTML snippets

In Angular, directives essentially allow you to create HTML snippets. A single snippet can be used by as many different controllers and views as you like. This is a really handy feature for making your application more consistent and easier to maintain. And because these snippets run in the context of an Angular application, they also come with all of the data-binding goodness we’ve seen so far.

As an added bonus, browsers can cache directives that are saved as HTML files, helping to speed up your application when users switch between views. But let’s back up a bit and start off by seeing how to add a directive to our application.

Adding a directive to an Angular application

We’ll start off in our Angular application to set up the code for a basic directive, and build up from there. A directive is added to an Angular application using the now-familiar module getter syntax and a named function. For the starting point of our directive we want to return a simple template that outputs the rating as a number, so in the browser it will look the same as it does now.

The following listing shows what needs to be added/updated in loc8rApp.js.

Listing 8.7. Creating a directive and adding it to the Angular module

This simple template is exactly the same as we currently have in the Jade template. It’s important that the name of the function is in camelCase—you’ll see why next when we update the Jade view to use this new directive.

Attaching a directive to the HTML template

Having a directive in the Angular application is all very well and good, but we need to tell the HTML where we want to use it. The default way to do this is to add an attribute into the tag that will contain the directive.

The attribute name is important, and must match that of the directive name, but in a different format. Unlike JavaScript, HTML isn’t case sensitive, so it doesn’t make sense to use camelCase here. Instead, the uppercase letters are converted to lowercase letters and prefixed with a dash. So the directive name ratingStars would be referenced in HTML as rating-stars.

In raw HTML, when applied to a div, it would look like the following:

<div rating-stars></div>

In our homepage we want to add this to a small tag. Jade templates allow us to add in attributes without a value, which is what we need here. In the following code snippet we do exactly this:

This simple change allows Angular to bind the ratingStars directive to the small tag, inserting the contents within it. If you reload the page now you shouldn’t actually see any changes, as all we’ve done is change the way that Angular outputs the number.

This number is a good start, but the idea of a directive is that it’s reusable. What we have is reusable, but next we’ll make it even more so by removing its reliance on having a value assigned to location.rating in the current scope. We’ll do this by using what’s known as an isolate scope.

Passing variables to directives with an isolate scope

Our current template for showing the rating will only work when the directive is included in a scope that contains a value for location.rating. This might seem fine for now, but what if we want to show the rating stars for each review in a list of reviews? That’s not likely to reference an object called location.rating, and even if it does it’s not likely to be the piece of data we actually want to use.

To address this problem we can create an isolate scope for the directive by adding the scope option to the directive’s definition. Listing 8.8 shows how to do this, creating a new scope variable thisRating for the directive. The value of '=rating' tells Angular to look for an attribute called rating on the same HTML element that defined the directive.

Listing 8.8. Updating the directive to use an isolate scope for the rating

This directive now expects the HTML element to which it’s bound to contain a rating attribute, which in turn holds the value of the rating. The following code snippet shows the update we need to make to the Jade template to make this happen:

This update means that wherever we want to use rating stars we can add two attributes to an HTML element, one to bind the directive and one to pass the rating value. So the directive is no longer dependent on a specific value existing in the parent scope.

After all that, it’s still not showing stars. We’ll address that now.

Using an external HTML file for the template

As a rule of thumb, unless a directive template is very simple it should exist in its own HTML file. Our directive to display rating stars will be a little more complex than just displaying the number sent to it, so we’ll put it into an external HTML file. As well as separating concerns—keeping markup separate from application logic—this approach has the added bonus of browsers being able to cache HTML files.

To move a template into an external HTML file, the first thing to do is to take template out of the directive definition and replace it with templateUrl. templateUrl should hold the path to the HTML file. The following listing shows how we can update the ratingStars directive to use a file, /angular/rating-stars.html.

Listing 8.9. Update the directive to use an external file for the template

This HTML file doesn’t exist yet, so we’ll create that now. In the /public/angular folder create a file called rating-stars.html. In that file we’ll start with the HTML needed to output a rating; the following code snippet shows the markup used to display a three-star rating:

To make the template smart we can use an Angular binding to insert the -empty class suffix where appropriate. For example, if the rating is less than two, only the first star should be solid, and the remaining four hollow. We’ll use the JavaScript ternary operator (a shorthand for a simple if-else conditional statement) to achieve this, as shown in the following listing.

Listing 8.10. Creating the Angular rating star template
<span class="glyphicon glyphicon-star{{ thisRating<1 ? '-empty' : ''}}"></span>
<span class="glyphicon glyphicon-star{{ thisRating<2 ? '-empty' : ''}}"></span>
<span class="glyphicon glyphicon-star{{ thisRating<3 ? '-empty' : ''}}"></span>
<span class="glyphicon glyphicon-star{{ thisRating<4 ? '-empty' : ''}}"></span>
<span class="glyphicon glyphicon-star{{ thisRating<5 ? '-empty' : ''}}"></span>

For each star, if thisRating is less than the number of stars, Angular will add the -empty suffix to the class. Reloading the page now gives us our stars back, as shown in figure 8.7.

Figure 8.7. The Angular directive for the rating is now complete and shows the rating stars back in place.

That finalizes sorting out how to get the list to display correctly using Angular. But what we set out to do was add a text input so that we could filter the results. Let’s do that now.

Filtering a list of results with Angular

Earlier in this chapter we saw how we could change the output of a particular piece of data by using a filter. Some quite astounding news is that we can do exactly the same thing with a list of results by adding a filter to the ng-repeat directive. We’re actually going to filter the list of locations on the homepage without using any JavaScript.

The aim is for the list to be filtered according to whatever a user types in, so we’re going to create an input box. We’ll bind an input field to the model, and then apply this as a filter to the ng-repeat directive. It’s really as simple as that.

The following listing shows how to set this up in the Jade template for the homepage.

Listing 8.11. Creating and applying the text filter for the list of results

That’s all there is to it. Think of all the effort you’d have to go to if you wanted to manually program this functionality from scratch! Before we look at a screenshot we’ll add a tweak to the CSS in public/stylesheets/style.css to prevent it from looking too cramped:

#filter {margin-left: 4px;}

Now we can see what it looks like and how it works in figure 8.8.

Figure 8.8. The text filter in action on the homepage, filtering the list of results as a user types

I think we can all agree that that’s pretty neat. But we must not forget that the data for this is currently hard-coded into the controller. In the next section we’ll see how to make use of our API and pull the data from the database.

8.3. Getting data from an API

In this section we’re going to do a couple of things. First we’re going to give ourselves a pat on the back for having the foresight to create a REST API for our database. When we’ve calmed down a bit we’ll use our API from Angular to get the data for the home-page, and then also make the page location-aware. This step will mean that the application will start to show places that are near you, not just near the coordinates you hard-coded in.

Okay, let’s get started. We’ll begin by moving the data out of the controller and into a service.

8.3.1. Using services for data

Services are self-contained units of functionality that can be combined to provide the complete functionality of a software application. You’ll end up using services quite a lot in Angular, as most application logic should be deferred to services, making it reusable from multiple controllers. In our case here, we’ll be creating a data service that we can use elsewhere.

When accessing data, a controller shouldn’t care where the data comes from; it just needs to know who to ask for it. This is what we’re going to do right now—move the data from the controller to a service, and have the controller call the service.

Creating a simple data service

The method of defining a service in Angular shouldn’t be much of a surprise by now. We’ll create a named function and then register the service with the application.

So let’s define a function called loc8rData and have it return the data that’s currently being held in the controller. We’ll then register it with our application. The following listing shows both of these steps.

Listing 8.12. Creating a simple data service and adding it to the module

That seems fairly intuitive, right? And in keeping with the patterns we’ve seen so far in this chapter.

Using a service from a controller

To use a service from a controller we first have to pass the service to the controller. Passing the service in is easy—just add the name of the service as a parameter in the function definition for the controller.

When the service has been passed to the controller it can be used, just as you might expect. The following code snippet shows how to update the controller function to accept the service and use it:

Reloading the application now shouldn’t show any changes in the homepage. The change we’ve made here is behind the scenes getting ready for the next step: getting the data from our API.

8.3.2. Making HTTP requests from Angular to an API

Making HTTP requests from JavaScript is nothing particularly new. jQuery has had good Ajax support for a while, and in chapter 7 we used the request module to make HTTP requests from Node. Angular has a built-in service called $http to manage this type of request.

We’ll now use the $http service to make the call to our API and retrieve the data for the homepage. This request will be asynchronous, of course, so we’ll also see how to handle that.

Adding and using the $http service

Adding the $http service to the application is very easy—in fact, we only need to use it in our data service. Remember that the controller shouldn’t care how the data is obtained, it just knows who to ask.

There are two key things we need to know about the $http service to get started with it:

  • The $http service is given to another service by passing it as a parameter to the function, much like we’ve done with $scope in the controller.
  • The $http service has a get method that expects a single parameter—the URL—to be called.

If we put these two things together we can update our data service function loc8rData to look like the following code snippet (remember to put in coordinates near to you or the locations you’ve added):

So now, instead of returning some hard-coded data, the data service returns to the call the $http.get method. If you reload the page now expecting to see the data in the homepage you’ll be disappointed. You’ll even be able to see in the JavaScript console of your browser (depending on your browser) that an XHR request to the API has loaded and returned some data.

So why isn’t the page showing the data? Well that’s because the $http.get is asynchronous, and our controller is trying to use it in a synchronous way. Let’s see how to use it properly.

Handling the asynchronous nature of $http

Because $http goes off dealing with web services, there’s no idea of how long it’s going to take to execute. Nobody wants their entire JavaScript program to stop working while we’re waiting for a response, so it makes sense that $http runs asynchronously.

Our controller code is currently just this:

var locationListCtrl = function ($scope, loc8rData) {
  $scope.data = { locations: loc8rData };
};

This is running synchronously, and as we’ve seen it will not work with $http. $http instead returns a promise with two methods: success and error. What this means is that rather than expecting the $http.get call to return data, it will invoke either its success method or its error method.

Let’s clarify this with a bit of code. loc8rData now returns the $http.get method, so we’ll start by invoking that. We then chain the success and error methods to it using the dot syntax. When loc8rData has received a response it will invoke either the success or error method, depending on the response.

The scaffolding for this looks like the following listing, which also shows how to take the data returned from a successful request and pass it into the scope.

Listing 8.13. Updating the controller to work asynchronously with the $http promises

With that in place we’ll once again be able to see the data from the database showing up on our homepage. This is the minimum functionality, but as the code is asynchronous it would be good to let the user know what’s happening while the empty page displays and the $http request is being made.

Telling the user what’s going on

A problem with asynchronous data calls in the client side is that a user will first see the page rendered without any content. This will last until the asynchronous call completes and returns some data. This isn’t great! If your page remains free of data for even just a very short time users may well see the empty page and click the back button.

So when making asynchronous calls for data, always let the user know that you’re doing something and that something is happening in the background. In Loc8r we’ll do this by outputting a simple message, largely because we already have the HTML in place for it.

Inside the Jade template we already have a div set up to hold a message, as highlighted in bold in the following code snippet:

.col-xs-12.col-sm-8(ng-controller="locationListCtrl")
  label(for="filter") Filter results
  input#filter(type="text", name="filter", ng-model="textFilter")
  .error {{ message }}

This div and message were part of the original Jade template when we were using Express to generate the final HTML. We updated message to be an Angular binding when we set up the Angular controller instead. So we can use this message binding in the scope in our controller. The following listing shows how to update the scope to show a different message at different points in the process.

Listing 8.14. Setting an output message at various points in the process

Just something as simple as that can make a huge difference to the user experience. If you want to take it to the next level and include an Ajax Spinner—usually an animated GIF of a spinning wheel—go ahead! But we’ll stick with what we’ve got for the time being because we’ve got something cool coming up.

Now we’re going to make our application show places that are actually near you, not just near the coordinates that you hard-coded in.

8.3.3. Adding HTML geolocation to find places near you

The main premise of Loc8r is that it will be location-aware, and thus able to find places that are near you. So far we’ve been faking it by hard-coding geographic coordinates into the API requests. We’re going to change that right now by adding in HTML5 geolocation.

To get this working we’ll need to do the following:

  • Add a call to the HTML5 location API into our Angular application.
  • Only look for places when we have the location.
  • Pass the coordinates to our Angular data service, removing the hard-coded location.
  • Output messages along the way so the user knows what’s going on.

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

Creating an Angular geolocation service

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

We won’t get distracted by diving into the details of how the HTML5/JavaScript geolocation API works right now. We’ll just say that 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);

We’ll need to wrap the standard geolocation script in a function so that we can use it as a service, and also error trap against the possibility that the current browser doesn’t support this. The following listing shows the code needed to create the geolocation service and provide a getPosition method that the controller can call.

Listing 8.15. Create a geolocation service returning a method to get the current position

That code gives us a geolocation service, with a method getPosition that we can pass three callback functions to. This service will check to see whether the browser supports geolocation and then attempt to get the coordinates. The service will then call one of the three different callbacks depending on whether geolocation is supported and whether it was able to successfully obtain the coordinates.

The next step is to add it to the application.

Adding the geolocation service to the application

To use our new geolocation service we need to register it with the application and inject it into the controller.

The following code snippet highlights in bold the addition we need to make to the module definition to register the service with the application.

angular
  .module('loc8rApp')
  .controller('locationListCtrl', locationListCtrl)
  .filter('formatDistance', formatDistance)
  .directive('ratingStars', ratingStars)
  .service('loc8rData', loc8rData)
  .service('geolocation', geolocation);

Once the geolocation service has been registered we then need to inject it into the locationListCtrl controller so that it can use it. To do this we just need to add the name of the service to the parameters being accepted by the controller function. The following code snippet demonstrates this (the contents of the controller aren’t included in this snippet for the sake of brevity):

This approach is probably looking familiar by now. The next step is to get the controller to interact with the service.

Using the geolocation service from the controller

The controller now has access to the geolocation service, so let’s use it! Calling it will be pretty easy; we just need to call geolocation.getPosition and pass it the three callbacks we want to use.

Inside the controller we’ll create three functions, one for each of the possible outcomes:

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

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

The following listing shows the controller after all of these updates are made, creating the new functions and calling the geolocation service to kick it off.

Listing 8.16. Updating the controller to use the new geolocation service

The first thing we did here is tell users that we’re finding their location by setting a default message . We then created a function to run when geolocation is successful . The native geolocation API passes a position object to this callback. This function will then call the loc8rData service to get the list of locations.

Next we have a function to run when geolocation is supported but not successful . The native geolocation API passes an error object to the callback containing a message property that we can output to the user. Note the $scope.$apply() wrapper and take a look at the sidebar to see what it is and why it’s there.

The noGeo function will run if geolocation isn’t supported by the browser, and set a message to be output to the user. Note again the use of $scope.$apply().

Finally, we call our geolocation service , passing the three callback functions as parameters.

About $scope.$apply()

In the code snippet that manages the geolocation functionality we use $scope.$apply() in two out of the three callbacks after updating a value in the scope. If we hadn’t done this, the messages wouldn’t have displayed in the browser. But why, and why only two of the callbacks?

$scope.$apply() is used when Angular may not know about some updates to the scope. This typically occurs after an asynchronous event, such as a callback or user action. The purpose of the function is to push changes in scope over to the view.

$scope.$apply() is actually used a lot in Angular, but most of the time you don’t see it. We didn’t use it with the success callback, because in that case the changes in scope were made within the returned promises of the $http service. Behind the scenes Angular wraps these promises inside a $scope.$apply() function so that you don’t have to. Most of the native Angular events, in fact, are wrapped inside one, saving you time and effort most of the time.

If you reload the application in your browser you’ll now be prompted to share your location, which you can allow or deny. If you deny the browser access to your location the $scope.showError callback function will be invoked and an error message will be displayed on the screen. Figure 8.9 shows how this looks in Chrome.

Figure 8.9. With geolocation added Loc8r will now ask your permission to know your location; if you say no then a simple message is displayed.

If you say no to test this error message there is a good chance that your browser will remember this setting. If it does you can manage the settings in your browser’s preferences or options to let you test what happens if you say yes.

If you do say yes then the application will call our data service to get the data from the API. This sounds good but we’ve missed one step: we haven’t passed the coordinates to the data service yet, so it’s still using the hard-coded lat and lng values. Let’s change this now.

Passing the geolocation coordinates to the data service

We’re almost there. The geolocation service is returning coordinates to the Angular application, so we now just need to get these coordinates over to our data service so that we can add them to the API call.

The first thing to do is modify the loc8rData service, wrapping the call to the API in a function so that we can pass data to it. We can’t pass parameters into the loc8rData function itself because it’s a service constructor. In the following listing we’ll set up the new nested function to accept two parameters, lat and lng, and update the call to the API to use them.

Listing 8.17. Updating the loc8rData service to return a function instead of data

Our loc8rData service now has a method locationByCoords that the controller can access, passing it the coordinates. So let’s make the update to the controller.

We’ve seen that the geolocation API returns a position parameter to the success callback, which in this case we’ve called $scope.getData. position is a JavaScript object containing various pieces of data, including a coords path that holds values for latitude and longitude.

The following listing shows how to update the $scope.getData function in the controller, modifying the call to the data service to use the new method and pass through the parameters.

Listing 8.18. Update the controller to pass the coordinates to the services

And that’s the last piece of the puzzle. Loc8r now finds your current location and lists the places near you, which was the whole idea from the very start. You can also filter the results on the homepage using the text input.

Figure 8.10 shows the homepage now. It doesn’t look much different from when we started this chapter, but the main content area of the homepage is now an Angular module.

Figure 8.10. Visually the only difference to the homepage from the start of the chapter is the addition of the filter textbox; under the hood, however, the main content area is now being generated by Angular, which is also finding your actual location and displaying results close by.

And that’s it. Nearly. As we’ve been merrily adding in Angular to the homepage we’ve actually introduced a bug on the Add Review page. Now we’ll see what that bug is and how to fix it.

8.4. Ensuring forms work as expected

By adding Angular to the site template in layout.jade we’ve inadvertently created a bug in the form that adds a review. When we created the review form we kept the action blank, as this meant that the form would submit to the current URL. This is exactly what we wanted and has worked well for us so far. So, what’s the problem?

When Angular encounters a form without an action it prevents the submit action from happening. In many ways this makes sense. If you’re creating an Angular application and include a form, you’ll probably want Angular to handle the form and manage the submission and destination. You don’t want to manually prevent it from submitting each time, so Angular helps you out by doing it for you.

This is very helpful, except for the situation we find ourselves in now. The way to fix it is to add an action to the form; this will stop Angular from interfering with the form.

Step one is to pass the current URL to Jade from the controller. An easy way to get an accurate representation of the URL is via the originalUrl property of the req object. The following listing shows the update required to the renderReviewForm function in locations.js in the app_server/controllers folder.

Listing 8.19. Pass the URL from the review form controller
var renderReviewForm = function (req, res, locDetail) {
  res.render('location-review-form', {
    title: 'Review ' + locDetail.name + ' on Loc8r',
    pageHeader: { title: 'Review ' + locDetail.name },
    error: req.query.err,
    url: req.originalUrl
  });
};

Step two is to simply output this url parameter in the action attribute of the form definition in location-review-form.jade in the app_server/views folder. This is shown in the following code snippet:

form.form-horizontal(action="#{url}", method="post", role="form")

With those two small updates the form will continue to work as before and Angular will ignore it.

8.5. Summary

In this chapter we’ve covered

  • Simple binding and two-way data binding
  • The best practice of using the getter and setter syntax for defining a module and its components
  • Defining controllers in code and HTML to manage the application
  • How scope works in Angular, particularly the rootScope and controller-level scopes
  • Creating HTML snippets with directives
  • Using filters to modify the output of text and also entire repeated items of HTML
  • Using services to add reusable pieces of functionality and work with data
  • The $http service for making asynchronous calls to an API
  • When and why to use $scope.$apply()
  • Working with the HTML5 location API

Coming up next in chapter 9 we’re going to start converting the whole of the Loc8r application into a single-page application using Angular. So take a deep breath and get ready—it’s going to be a good one!

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

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