This tutorial is designed to give backend developers a place to put Angular to work for them, today. It starts with a hypothetical site which has table of records rendered server side. The tutorial gently guides the reader through moving the rendering from the server and to the client with Angular and uses the momentum from that to add two new features:
- Sort table by column
- Filter table by input field
This is the server side template which renders a collection of songs.
<table>
<thead>
<tr>
<th>Name</th>
<th>Artist</th>
<th>Album</th>
<th>Time</th>
<th>Download</th>
</tr>
</thead>
<tbody>
<% songs.each do |song| %>
<tr>
<td><%= song.name %></td>
<td><%= song.artist %></td>
<td><%= song.album %></td>
<td><%= song.time %></td>
<td><%= link_to "Download", song.file %></td>
</tr>
<% end %>
</tbody>
</table>
This tutorial assumes that you have access to frontend tooling for linting, compilation, concatenation, and minification. We use lineman.js at test double for our build process and quite like it.
Our first step is the Folger's challenge. We're going to move the rendering to the client without changing any functionality. Before writing any JavaScript, you'll need to pull in the angular.js library.
Add the ng-app attribute to a parent element of the table. Scope it as close as possible if you're nervous or just add it to the html tag if you don't want to think about it.
<html ng-app="mixtape">
We're going to change that server side template into a flat HTML file with Angular markup. (A video on the ng.repeat
filter)
<table ng-controller="songListCtrl">
<thead>
<tr>
<th>Name</th>
<th>Artist</th>
<th>Album</th>
<th>Time</th>
<th>Download</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="song in songs">
<td>{{ song.name }}</td>
<td>{{ song.artist }}</td>
<td>{{ song.album }}</td>
<td>{{ song.time }}</td>
<td><a href="{{ song.download }}">Download</a></td>
</tr>
</tbody>
</table>
We declare our angular module.
mixtape.js
angular.module("mixtape", []);
Add our controller. (A video on controllers.)
controllers/song_list_ctrl.js
angular.module("mixtape").controller("songListCtrl", function($scope) {
return $scope.songs = app.songFixture;
});
A temporary fixture file so we won't have to worry about an ajax request quite yet.
fixtures/song_fixture.js
window.app = app || {};
//Temporary fixture
window.app.songFixture = [{
name: "My Love",
artist: "The Bird and the Bee",
album: "Ray Guns Are Not Just The Future",
time: "3:46",
download: "https://example.s3.amazonaws.com/path/02_My_Love.m4a",
url: "http://example.com/songs/1.json",
path: "/songs/1"
},
{
name: "Team",
artist: "Lorde",
album: "Pure Heroine",
time: "3:13",
download: "https://example.amazonaws.com/path/Lorde_-_Team__Clean_.mp3",
url: "http://example.com/songs/30.json",
path: "/songs/30"
}]
Our page should look identical other than us only displaying two songs.
It's time to pull in real data and render it. First we'll need to expose restful routes with our server side code of choice.
Here are the routes I'm exposing for songs.
GET /songs songs#index
POST /songs songs#create
GET /songs/new songs#new
GET /songs/:id/edit songs#edit
GET /songs/:id songs#show
PATCH /songs/:id songs#update
PUT /songs/:id songs#update
DELETE /songs/:id songs#destroy
Next we'll need to download and include the separate angular-resource.js to get access to the $resource
provider. Once we have that file included in our dependencies, we'll add it to our module declaration.
mixtape.js
angular.module("mixtape", ["ngResource"]);
Next we'll need an object that knows how to interact with those RESTful routes. (A video on services.)
models/song.js
// use "/songs/:id.json" in Rails.
angular.module("mixtape").factory("Song", function($resource) {
return $resource("/songs/:id");
});
Point our controller at our Song
object instead of the fixture.
controllers/song_list_ctrl.js
angular.module("mixtape").controller("songListCtrl", function($scope, Song) {
return $scope.songs = Song.query();
});
Delete fixtures/song_fixture.js
We're now at feature parity with the server side code.
No change to our JavaScript for this feature. (Read about ng.filter:orderBy
.)
<table ng-controller="songListCtrl">
<thead>
<tr>
<th><a href="" ng-click="predicate = 'name'; reverse=!reverse">Name</a></th>
<th><a href="" ng-click="predicate = 'artist'; reverse=!reverse">Artist</a></th>
<th><a href="" ng-click="predicate = 'album'; reverse=!reverse">Album</a></th>
<th><a href="" ng-click="predicate = 'time'; reverse=!reverse">Time</a></th>
<th>Download</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="song in songs | orderBy:predicate:reverse">
<td>{{ song.name }}</td>
<td>{{ song.artist }}</td>
<td>{{ song.album }}</td>
<td>{{ song.time }}</td>
<td><a href="{{ song.download }}">Download</a></td>
</tr>
</tbody>
</table>
Seriously, that's it.
Once again, no change to our JavaScript. (Read about filtering repeaters.) Here are the changes to the HTML in isolation.
Filter: <input ng-model="query">
...
<tr ng-repeat="song in songs | orderBy:predicate:reverse | filter:query">
...
And the whole file.
Filter: <input ng-model="query">
<table ng-controller="songListCtrl">
<thead>
<tr>
<th><a href="" ng-click="predicate = 'name'; reverse=!reverse">Name</a></th>
<th><a href="" ng-click="predicate = 'artist'; reverse=!reverse">Artist</a></th>
<th><a href="" ng-click="predicate = 'album'; reverse=!reverse">Album</a></th>
<th><a href="" ng-click="predicate = 'time'; reverse=!reverse">Time</a></th>
<th>Download</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="song in songs | orderBy:predicate:reverse | filter:query">
<td>{{ song.name }}</td>
<td>{{ song.artist }}</td>
<td>{{ song.album }}</td>
<td>{{ song.time }}</td>
<td><a href="{{ song.download }}">Download</a></td>
</tr>
</tbody>
</table>
We can build out more lightweight frontend features from here:
- pagination
- in-place CRUD
- async, parellel file uploads
The goal of this tutorial was providing a recipe for making small changes to the functionality of an app with Angular. We wanted the changes to be small to give us a place to use Angular without rewriting the existing codebase which makes it easier to get started.