A Step-by-Step Guide to Your First AngularJS App
What is AngularJS?
AngularJS is a JavaScript MVC framework developed by Google that lets you build well structured, easily testable, and maintainable front-end applications.
And Why Should I Use It?
If you haven’t tried AngularJS yet, you’re missing out. The framework consists of a tightly integrated toolset that will help you build well structured, rich client-side applications in a modular fashion—with less code and more flexibility.
AngularJS extends HTML by providing directives that add functionality to your markup and allow you to create powerful dynamic templates. You can also create your own directives, crafting reusable components that fill your needs and abstracting away all the DOM manipulation logic.
It also implements two-ways data binding, connecting your HTML (views) to your JavaScript objects (models) seamlessly. In simple terms, this means that any update on your model will be immediately reflected in your view without the need for any DOM manipulation or event handling (e.g., with jQuery).
Finally, I love Angular because of its flexibility regarding server communication. Like most JavaScript MVC frameworks, it lets you work with any server-side technology as long as it can serve your app through a RESTful web API. But Angular also provides services on top of XHR that dramatically simplify your code and allow you to abstract API calls into reusable services. As a result, you can move your model and business logic to the front-end and build back-end agnostic web apps. In this post, we’ll do just that, one step at a time.
So, Where Do I Begin?
First, let’s decide the nature of the app we want to build. In this guide, we’d prefer not to spend too much time on the back-end, so we’ll write something based on data that’s easily attainable on the Internet—like a sports feed app!
Since I happen to be a huge fan of motor racing and Formula 1, I’ll use an autosport API service to act as our back-end. Luckily, the guys at Ergast are kind enough to provide a free motorsport API that will be perfect for us.
For a sneak peak at what we’re going to build, take a look at the live demo. To prettify the demo and show off some Angular templating, I applied the awesome Plastic Admin Bootstrap theme; but seeing as this article isn’t about CSS, I’ll just abstract it away from the examples and leave it out.
Getting Started
Let’s kickstart our example app with some boilerplate. I recommend the angular-seed project as it not only provides you with a great skeleton for bootstrapping, but also sets the ground for unit testing with Karma andJasmine (we won’t be doing any testing in this demo, so we’ll just leave that stuff aside for now).
You app’s skeleton will look like this:
Now we can start coding. As we’re trying to build a sports feed for a racing championship, let’s begin with the most relevant view: the championship table.
Given that you already have a drivers list defined within your scope (hang with me—we’ll get there), and ignoring any CSS (for readability), your HTML might look like:
<body ng-app="F1FeederApp" ng-controller="driversController">
<table>
<thead>
<tr><th colspan="4">Drivers Championship Standings</th></tr>
</thead>
<tbody>
<tr ng-repeat="driver in driversList">
<td>{{$index + 1}}</td>
<td>
<img src="img/flags/{{driver.Driver.nationality}}.png" />
{{driver.Driver.givenName}} {{driver.Driver.familyName}}
</td>
<td>{{driver.Constructors[0].name}}</td>
<td>{{driver.points}}</td>
</tr>
</tbody>
</table>
</body>
The first thing you’ll notice in this template is the use of expressions (“{{“ and “}}”) to return variable values. In AngularJS, expressions allow you to execute some computation in order to return a desired value. Some valid expressions would be:
{{ 1 + 1 }}
{{ 946757880 | date }}
{{ user.name }}
Effectively, expressions are JavaScript-like snippets. But despite being very powerful, you shouldn’t use expressions to implement any higher-level logic. For that, we use directives.
Understanding Basic Directives
The second thing you’ll notice is the presence of ng-attributes
, which you wouldn’t see in typical markup. Those are directives.
At a high level, directives are markers (such as attributes, tags, and class names) that tell AngularJS to attach a given behaviour to a DOM element (or transform it, replace it, etc.). Let’s take a look at the ones we’ve seen already:
-
The
ng-app
directive is responsible for bootstrapping your app defining its scope. In AngularJS, you can have multiple apps within the same page, so this directive defines where each distinct app starts and ends. -
The
ng-controller
directive defines which controller will be in charge of your view. In this case, we denote thedriversController
, which will provide our list of drivers (driversList
). -
The
ng-repeat
directive is one of the most commonly used and serves to define your template scope when looping through collections. In the example above, it replicates a line in the table for each driver indriversList
.
Adding Controllers
Of course, there’s no use for our view without a controller. Let’s add driversController
to our controllers.js:
angular.module('F1FeederApp.controllers', []).
controller('driversController', function($scope) {
$scope.driversList = [
{
Driver: {
givenName: 'Sebastian',
familyName: 'Vettel'
},
points: 322,
nationality: "German",
Constructors: [
{name: "Red Bull"}
]
},
{
Driver: {
givenName: 'Fernando',
familyName: 'Alonso'
},
points: 207,
nationality: "Spanish",
Constructors: [
{name: "Ferrari"}
]
}
];
});
You may have noticed the $scope
we’re passing as parameter to the controller. The $scope
variable is supposed to link your controller and views. In particular, it holds all the data that will be used within your template. Anything you add to it (like the driversList
in the above example) will be directly accessible in your views. For now, lets just work with a dummy (static) data array, which we will replace later with our API service.
Now, add this to app.js:
angular.module('F1FeederApp', [
'F1FeederApp.controllers'
]);
With this line of code, we actually initialize our app and register the modules on which it depends. We’ll come back to that file (app.js) later on.
Now let’s put everything together in index.html:
<!DOCTYPE html>
<html>
<head>
<title>F-1 Feeder</title>
<script src="lib/angular/angular.js"></script>
<script src="lib/angular/angular-route.js"></script>
<script type="text/javascript" src="js/app.js"></script>
<script type="text/javascript" src="js/controllers.js"></script>
<script type="text/javascript" src="js/services.js"></script>
</head>
<body ng-app="F1FeederApp" ng-controller="driversController">
<table>
<thead>
<tr><th colspan="4">Drivers Championship Standings</th></tr>
</thead>
<tbody>
<tr ng-repeat="driver in driversList">
<td>{{$index + 1}}</td>
<td>
<img src="img/flags/{{driver.Driver.nationality}}.png" />
{{driver.Driver.givenName}} {{driver.Driver.familyName}}
</td>
<td>{{driver.Constructors[0].name}}</td>
<td>{{driver.points}}</td>
</tr>
</tbody>
</table>
</body>
</html>
Modulo minor mistakes, you can now boot up your app and check your (static) list of drivers.
Note: If you need help debugging your app and visualizing your models and scope within the browser, I recommend taking a look at the awesome Batarang plugin for Chrome.
Loading Data From the Server
Since we already know how to display our controller’s data in our view, it’s time to actually fetch live data from a RESTful server.
To facilitate communication with HTTP servers, AngularJS provides the $http
and $resource
services. The former is but a layer on top of XMLHttpRequest or JSONP, while the latter provides a higher level of abstraction. We’ll use $http
.
To abstract our server API calls from the controller, let’s create our own custom service which will fetch our data and act as a wrapper around $http
. Add this to your services.js:
angular.module('F1FeederApp.services', []).
factory('ergastAPIservice', function($http) {
var ergastAPI = {};
ergastAPI.getDrivers = function() {
return $http({
method: 'JSONP',
url: 'http://ergast.com/api/f1/2013/driverStandings.json?callback=JSON_CALLBACK'
});
}
return ergastAPI;
});
With the first two lines, we create a new module (F1FeederApp.services
) and register a service within that module (ergastAPIservice
). Notice that we pass $http
as a parameter to that service. This tells Angular’sdependency injection engine that our new service requires (or depends on) the $http
service.
In a similar fashion, we need to tell Angular to include our new module into our app. Let’s register it with app.js, replacing our existing code with:
angular.module('F1FeederApp', [
'F1FeederApp.controllers',
'F1FeederApp.services'
]);
Now, all we need to do is tweak our controller.js a bit, include ergastAPIservice
as a dependency and we’ll be good to go:
angular.module('F1FeederApp.controllers', []).
controller('driversController', function($scope, ergastAPIservice) {
$scope.nameFilter = null;
$scope.driversList = [];
ergastAPIservice.getDrivers().success(function (response) {
//Dig into the responde to get the relevant data
$scope.driversList = response.MRData.StandingsTable.StandingsLists[0].DriverStandings;
});
});
Now reload your app and check out the result. Notice that we didn’t make any changes to our template, but we added a nameFilter
variable to our scope. Let’s put that variable to use.
Filters
Great! We have a functional controller. But it only shows a list of drivers. Let’s add some functionality by implementing a simple text search input which will filter our list. Add the following line to your index.html, right below the <body>
tag:
<input type="text" ng-model="nameFilter" placeholder="Search..."/>
We are now making use of the ng-model
directive; this directive will bind your text field to the $scope.nameFilter
variable and make sure that its value is always up-to-date with the input value. Now, let’s visit index.html one more time and make a small adjustment to the line that contains the ng-repeat
directive.
<tr ng-repeat="driver in driversList | filter: nameFilter">
This line tells ng-repeat
that before outputting the data, the drivers array must be filtered by the value stored in nameFilter
.
At this point, two-way data binding kicks in: every time you input some value in the search field, Angular immediately ensures that the $scope.nameFilter
that you associated to it is updated with the new value. Since the binding works both ways, the moment the nameFilter
value is updated, the second directive associated to it (the ng-repeat
) also gets the new value and the view is updated immediately.
Reload your app and check out your search bar.
Notice that this filter will look for the keyword on all attributes of the model, including the ones you´re not using. Let’s say you only want to filter by Driver.givenName
and Driver.familyName
: First add to your driversController
, right below the $scope.driversList = [];
line:
$scope.searchFilter = function (driver) {
var keyword = new RegExp($scope.nameFilter, 'i');
return !$scope.nameFilter || keyword.test(driver.Driver.givenName) || keyword.test(driver.Driver.familyName);
};
Now, back to index.html, update the line that contains the ng-repeat
directive:
<tr ng-repeat="driver in driversList | filter: searchFilter">
Reload the app one more time and now you have a search by name.
Routes
Our next goal is to create a driver details page which will let us click on each driver and see his/her career details.
First, let’s include the $routeProvider
service (in app.js) which will help us deal with these varied application routes. Then, we’ll add two such routes: one for the championship table, and another for the driver details. Here’s our new app.’s:
angular.module('F1FeederApp', [
'F1FeederApp.services',
'F1FeederApp.controllers',
'ngRoute'
]).
config(['$routeProvider', function($routeProvider) {
$routeProvider.
when("/drivers", {templateUrl: "partials/drivers.html", controller: "driversController"}).
when("/drivers/:id", {templateUrl: "partials/driver.html", controller: "driverController"}).
otherwise({redirectTo: '/drivers'});
}]);
With that change, navigating to “http://domain/#/drivers” will load the driversController
and look for the partial view to render in “partials/drivers.html”. But wait! We don’t have any partial views yet, right? We’ll need to create those too.
Partial Views
AngularJS will allow you to bind your routes to specific controllers and views. But first, we need to tell Angular where to render these partial views. For that, we’ll use the ng-view
directive. Modify your index.html to mirror the following:
<!DOCTYPE html>
<html>
<head>
<title>F-1 Feeder</title>
<script src="lib/angular/angular.js"></script>
<script src="lib/angular/angular-route.js"></script>
<script type="text/javascript" src="js/app.js"></script>
<script type="text/javascript" src="js/controllers.js"></script>
<script type="text/javascript" src="js/services.js"></script>
</head>
<body ng-app="F1FeederApp">
<ng-view></ng-view>
</body>
</html>
Now, whenever you navigate through your app routes, Angular will load the associated view and render it in place of the <ng-view>
tag. All you need to do is create a file called partials/drivers.html and put your championship table HTML there. We’ll also use this chance to link the driver name to our driver details route:
<input type="text" ng-model="nameFilter" placeholder="Search..."/>
<table>
<thead>
<tr><th colspan="4">Drivers Championship Standings</th></tr>
</thead>
<tbody>
<tr ng-repeat="driver in driversList | filter: searchFilter">
<td>{{$index + 1}}</td>
<td>
<img src="img/flags/{{driver.Driver.nationality}}.png" />
<a href="#/drivers/{{driver.Driver.driverId}}">
{{driver.Driver.givenName}} {{driver.Driver.familyName}}
</a>
</td>
<td>{{driver.Constructors[0].name}}</td>
<td>{{driver.points}}</td>
</tr>
</tbody>
</table>
Finally, let’s decide what we want to show in the details page. How about a summary of all the relevant facts about the driver (e.g., birth, nationality) along with a table containing his/her recent results. Add to services.js:
angular.module('F1FeederApp.services', [])
.factory('ergastAPIservice', function($http) {
var ergastAPI = {};
ergastAPI.getDrivers = function() {
return $http({
method: 'JSONP',
url: 'http://ergast.com/api/f1/2013/driverStandings.json?callback=JSON_CALLBACK'
});
}
ergastAPI.getDriverDetails = function(id) {
return $http({
method: 'JSONP',
url: 'http://ergast.com/api/f1/2013/drivers/'+ id +'/driverStandings.json?callback=JSON_CALLBACK'
});
}
ergastAPI.getDriverRaces = function(id) {
return $http({
method: 'JSONP',
url: 'http://ergast.com/api/f1/2013/drivers/'+ id +'/results.json?callback=JSON_CALLBACK'
});
}
return ergastAPI;
});
This time, we provide the driver’s ID to the service so that we retrieve the information relevant solely to a specific driver. Now, modify controllers.js:
angular.module('F1FeederApp.controllers', []).
/* Drivers controller */
controller('driversController', function($scope, ergastAPIservice) {
$scope.nameFilter = null;
$scope.driversList = [];
$scope.searchFilter = function (driver) {
var re = new RegExp($scope.nameFilter, 'i');
return !$scope.nameFilter || re.test(driver.Driver.givenName) || re.test(driver.Driver.familyName);
};
ergastAPIservice.getDrivers().success(function (response) {
//Digging into the response to get the relevant data
$scope.driversList = response.MRData.StandingsTable.StandingsLists[0].DriverStandings;
});
}).
/* Driver controller */
controller('driverController', function($scope, $routeParams, ergastAPIservice) {
$scope.id = $routeParams.id;
$scope.races = [];
$scope.driver = null;
ergastAPIservice.getDriverDetails($scope.id).success(function (response) {
$scope.driver = response.MRData.StandingsTable.StandingsLists[0].DriverStandings[0];
});
ergastAPIservice.getDriverRaces($scope.id).success(function (response) {
$scope.races = response.MRData.RaceTable.Races;
});
});
The important thing to notice here is that we just injected the $routeParams
service into the driver controller. This service will allow us to access our URL parameters (for the :id
, in this case) using $routeParams.id
.
Now that we have our data in the scope, we only need the remaining partial view. Create a file called partials/driver.html and add:
<section id="main">
<nav id="secondary" class="main-nav">
<div class="driver-picture">
<div class="avatar">
<img ng-show="driver" src="img/drivers/{{driver.Driver.driverId}}.png" />
<img ng-show="driver" src="img/flags/{{driver.Driver.nationality}}.png" /><br/>
{{driver.Driver.givenName}}<br/>{{driver.Driver.familyName}}
</div>
</div>
<div class="driver-status">
Country: {{driver.Driver.nationality}} <br/>
Team: {{driver.Constructors[0].name}}<br/>
Birth: {{driver.Driver.dateOfBirth}}<br/>
<a href="{{driver.Driver.url}}" target="_blank">Biography</a>
</div>
</nav>
<div class="main-content">
<table class="result-table">
<thead>
<tr><th colspan="5">Formula 1 2013 Results</th></tr>
</thead>
<tbody>
<tr>
<td>Round</td> <td>Grand Prix</td> <td>Team</td> <td>Grid</td> <td>Race</td>
</tr>
<tr ng-repeat="race in races">
<td>{{race.round}}</td>
<td><img src="img/flags/{{race.Circuit.Location.country}}.png" />{{race.raceName}}</td>
<td>{{race.Results[0].Constructor.name}}</td>
<td>{{race.Results[0].grid}}</td>
<td>{{race.Results[0].position}}</td>
</tr>
</tbody>
</table>
</div>
</section>
Notice that we’re now putting the ng-show
directive to good use. This directive will only show the HTML element if the expression you provided is true
(i.e., neither false
, nor null
). In this case, the avatar will only show up once the driver object has been loaded into the scope by the controller.
Finishing Touches
Add in a bunch of CSS and render your page. You should end up with something like this:
You’re now ready to fire up your app and make sure both routes are working as desired. You could also add a static menu to index.html to improve the user’s navigation capabilities. The possibilities are endless.
Conclusion
At this point in the tutorial, we’ve covered everything you’d need to write a simple app (like a Formula 1 feeder). Each of the remaining pages in the live demo (e.g., constructor championship table, team details, calendar) share the same basic structure and concepts that we’ve reviewed here.
Finally, keep in mind that Angular is a very powerful framework and we’ve barely scratched the surface in terms of everything it has to offer. In a future post, we’ll give examples of why Angular stands out among its peer front-end MVC frameworks: testability. We’ll review the process of writing and running unit tests withKarma, achieving continuous integration with Yeomen, Grunt, and Bower, and other strengths of this fantastic front-end framework.
posted on 2014-04-29 02:52 Step-BY-Step 阅读(424) 评论(0) 编辑 收藏 举报