Skip to content

Instantly share code, notes, and snippets.

@joelip
Created May 31, 2014 03:03
Show Gist options
  • Save joelip/9a51adf7f2996d682810 to your computer and use it in GitHub Desktop.
Save joelip/9a51adf7f2996d682810 to your computer and use it in GitHub Desktop.

To dive into Angular, this checkpoint will guide you through angularizing the album view.

Background: Angular Controllers

An Angular controller, like its name suggests, is the controller in the model-view-controller design pattern. The controller allows us to access specific methods and data that can be used in our HTML views or other components through the $scope object (more on that below).

To declare a controller we use the syntax:

module = angular.module('ModuleName');

module.controller('ControllerName', ['dependence1', 'dependence2', ...,  function(dependence1, dependence2, ...) {
  // JavaScript that defines the behavior of the controller.
}]);

We've created a module here to encapsulate all of our related code. Next, we add our controller to the module by calling the controller function. The first argument is the name of the new controller. After that, we pass a second argument that is an array of dependencies, as well as a function. The function takes the dependencies as arguments and defines the logic for the controller.

Angular provides a shorter way of passing the second argument where it will use the name of the variable to figure out what it depends on. However, if the JavaScript minifier (a tool used to reduce script sizes) that you use isn't aware of this feature, it might change the name to something shorter and break your app.

For now, we're going to use the longer version. However, be careful to make sure that you keep the number of dependency names and variables the same. It's important that the dependencies are in the same order in both the array of strings and the arguments of your function.

To provide a controller (and its $scope variables and functions) to the elements in the DOM, we attach an ngController directive as a data attribute on the HTML. There are several conventions for doing this, shown below:

// with a dash
.container(ng-controller="Controller")
// with a an underscore
.container(ng_controller="Controller")
// preceded by 'data'
.container(data-ng-controller="Controller")
// preceded by 'x'
.container(x-ng-controller="Controller")

When referring to directives in our javascript, we use camelcase, like ngController. When we use it in our views, however, we use one of the conventions above. The nuances of the use cases will be explained in more detail when we get to our Angular directives checkpoint.

For the purposes of this course, and to avoid incompatibilty with certain templating languages, we will be using the underscore for attaching our directives to elements.

A note about Internet Explorer compatibility

Some directives can be used as a custom element on their own (meaning they don't need to be an attribute on the HTML element itself). This causes compatibility issues with Internet Explorer 8 and below, that are detailed in the Angular documentation.

As of April 2014, this subset of browsers only comprises about 2.6% of active internet users so it's not something that we'll be worrying about in this course. Just be aware that there are potential compatibility issues if you are programming for earlier browsers.

Background: Angular $scope

A second major component of Angular architecture is the $scope object, which encapsulates data and communicates between different components of the application. For this checkpoint it's important to understand several things about scopes:

  1. A $scope object is created automatically for each Angular controller and can be accessed by declaring $scope as one of the controllers dependencies.

  2. The $scope allows for your controller to provide data and functionality to the view.

  3. Changes to $scope variables automatically propagate to the view. That is, the changes are updated simultaneously with the view because data is shared between the controller and the view with data-binding, which we'll discuss below.

  4. Scopes can be nested in two different ways: with child scopes or isolate scopes. Child scopes inherit from parent scopes while isolate scopes do not inherit from any other scopes.

  5. Angular applications also have a single $rootScope object which is the parent scope for all other scopes in the application.

A quick $scope example

Let's take a look at an example of how we attach a variable to our $scope that can be referenced in the view:

In our controller

module = angular.module('ModuleName');

// pass the $scope object into our dependencies so that we can use it in our controller
module.controller('ControllerName', ['$scope', function($scope) {
	// attach a property to the object that we'll be using to hold our data that we want to access in the view
	$scope.title = 'Our new Angular app';
}]);

In our view

.container(ng_controller="ControllerName")
	h2 {{title}}

This will produce an <h2> with the contents 'Our new Angular app' that will change if we update $scope.title to something else.

Note that our $scope properties must be nested in an element with an ng_controller attribute in order for the data to be accessed properly. Because our scope variable is nested under the element with the controller attribute, the $scope object is implicit in all of our variables and functions that we use in our view. There is no need to precede the variable with $scope, as we do in our controller.

The {{}} double curly braces bind our variable to the contents of our view.

Angular Directive: ngRepeat

The last thing we'll need to understand to flesh out a basic Album view is the ability to display our list of songs. Angular has a built-in directive to repeat an HTML template a list of named ngRepeat. An ngRepeat is like a Jade each loop that repeats the same HTML and any data attached to individual objects in the passed array for each item in a list.

Here's how you convert a Jade each loop into an angular ngRepeat

// A table that's created on the server.
.table
  each color, index in ['Blue', 'Red', 'Green']
    tr
      td. #{index}
      td. #{color}

// A table that's created in the browser with Angular's ngRepeat directive.
.table
  tr(ng_repeat="color in ['Blue', 'Red', 'Green']")
    td. {{$index}}
    td. {{color}}

Angularizing the Album view

Now that you have had a basic introduction to controllers and scopes, let's put that knowledge into practice by implementing our Album view using Angular. The last time we touched this view, we had added jQuery to album.js to generate the Album content dynamically. For this excercise, we're going to convert this code into an equivalent Angular setup.

  1. Comment out our jQuery code - To start we'll need to comment out the code in app/scripts/album.js so that our jQuery code doesn't affect the DOM. You can delete this code later, but for right now it'll be useful to help you understand the differences between implementing the page in Angular vs. jQuery.

  2. Implement the 'Album.controller' - Next, we'll open up app/scripts/controllers/album_controller.js to implement our Angular controller for the page. We've already setup the basic structure of the controller that you'll need for this checkpoint. As you can see in the file we decided to organize our modules by purpose for this project. Our controllers will go in the "JamsControllers" module, our models will go in the "JamsModels" module, and so on. Secondly, we decided upon a naming convention where controllers will have the name of the page they control with .controller appended to the end, e.g. 'Album.controller for this page. While the actual names don't matter much it's always good to abide by a specific naming convention. Consistency is very important when writing code, markup or styling. It makes it easier for you and others to understand your code, which becomes increasingly important when dealing with large codebases.

When creating controllers, it's good practice to keep them small by giving them a single responsibility, e.g. make it responsible for a single page or widget. Following this advice, we'll be creating a controller for each of our views and each of our components starting with our first view.

Now, to fill in the controller we'll have to decide what interface we'll be providing to the view (i.e. the Angular directives used inside of the DOM). From our previous implementation, we know that we'll need to provide access to at least the information about the album that we're displaying, and secondly offer a way to switch between albums. Below is an example of how you could provide this interface.

angular.module('Controllers').controller('Album.controller', ['$scope', function($scope) {
  var albums = [albumPicasso, albumMarconi];
  var currentAlbumIndex = 0;

  // Remember: All properties added to the '$scope' object can be accessed in the 
  $scope.album = albums[0];
  $scope.changeAlbum = function() {
    currentAlbumIndex = (currentAlbumIndex + 1) % albums.length;
    $scope.album = albums[currentAlbumIndex];
  };
}]);

And, that's all we need to do for the controller. The bulk of the code that we added for jQuery was to handle changing the DOM elements and attaching event listeners to the DOM elements. With Angular, the remaining jQuery code can instead be placed directly into the Jade file.

####Modifying the view to display the current album

The next step in this process is to add the necessary HTML and Angular directives to our express/views/album.jade file. To start off will remind you of what the Jade looked like when we first implemented the album page. You don't have to completely replace your code, just make sure to follow along and make similar modifications.

extends layouts/player_layout

block content
  .container.album-container
    .row.album-header-container
      .col-md-3
        img(src="/images/album-placeholder.png")
      .col-md-9.col-md-push-1
        .album-header-information
          h3.album-title The Colors
          h4.album-artist Pablo Picasso
          h5.album-meta-info 1881 on Cubism
    .row
      .col-md-12
        table.table.album-song-listing
          each colorName, index in ['Blue', 'Green', 'Red', 'Pink', 'Magenta']
            //- This is a loop body
            tr
              td
                //- 'index' tells us which position we are in the array, this will go from 0 to 4.
                      * You can add a variable to the html document through interpolation "#{...}"
                | #{index + 1}
              td
                //- colorName is assigned the value of each element list of colors in turn: 
                      e.g. first 'Blue', then 'Green', then 'Red', and so on.
                | #{colorName}
              td
                | X:XX

Our first step when adding Angular to a page will be to add our controller (Album.controller) to one the top level container divs like so:

block content
  .container.album-container(ng_controller="Album.controller")
    //- Remaining code...

What this does is use the Angular ngController directive to let the Angular runtime system know that we want use our 'Album.controller' controller on this page, and that every child DOM element should, by default, have access to the controller's $scope.

Now with the controller in place, we can change our Jade code into an Angular template to have Angular automatically create the page's content. What we've done in the below segment is change the hard coded data about the album to a template that uses Angular's HTML interpolation {{...}} feature to display the album information. You can use any valid JavaScript inside of the interpolation braces (the {{ and }}), and you can also access any of the variables in the $scope of the controller or the parent $scope(s), i.e. we use the $scope.album variable that we setup in our controller as the source of our title, artist, and meta information.

block content
  .container.album-container
    .row.album-header-container
      .col-md-3
        img(src="/images/album-placeholder.png")
      .col-md-9.col-md-push-1
        .album-header-information
          h3.album-title {{ album.name }}
          h4.album-artist {{ album.artist }}
          h5.album-meta-info {{ album.year }} on {{ album.label }}

If you look at the page (localhost:3000/album) at this point, you should see all the top information about the album being set correctly.

Next, we'll update the src attribute of the album image to use the $scope.album data. The correct way to have Angular dynamically change the src of an image is to use the Angular directive ngSrc as follows:

//- code snippet.
img(ng_src="{{album.albumArtUrl}}")

Now, it's time to generate our list of songs. For this one, we'll use the ngRepeat directive as follows:

//- code
.row
  .col-md-12
    table.table.album-song-listing
      // Create the following DOM elements for each song in the array 'album.songs'.
      tr(ng_repeat="song in album.songs")
        // $index is provided by the 'ngRepeat'
        td
          | {{$index + 1}}
        td
          | {{song.name}}
        td
          | {{song.length}}

This directive is a little more complex because it allows for a variety of expressions on the right side of the ng_repeat.

####Adding the click handler

As a last step, we'll add the click event handler that we used on the image to swap albums. Angular provides us with a built-in directive for mouse events, and the one we'll use for mouse clicks is ngClick. This change is rather simple, we'll just attach the ngClick directive to our img and call our controller's changeAlbum function like so:

//- code
block content
  .container.album-container
    .row.album-header-container
      .col-md-3
        img(ng_src="{{album.albumArtUrl}}", ng_click="changeAlbum()")

With this addition, you should see the album information change every time that you click on the album art.

Debugging Angular

##*****Couldn't remember what you wanted to cover here, but let me know if I can help write it.

  • Angular controllers.

    • Purpose = Act as the glue between our data (remote and local) and the DOM = Set up the data that we'll need for the page. = Provides an interface for the directives to in the DOM.

    • Usage

    • Uses

  • Angular Scopes

    • Purpose
    • Usage
    • Uses
  • Angular two-way binding : interrelation between controllers-scopes-views.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment