Getting Started with AngularJS
I was hired by my current client in November to help them choose a technology stack for developing modern web applications. In our first sprint, we decided to look at JavaScript MVC frameworks. I suggested AngularJS, Ember.js and React. Since most of the team was new to JavaScript MVC, I decided to create a tutorial for them. I tried to make it easy so they could learn how to write a simple web application with AngularJS. I thought others could benefit from this article as well, so I asked (and received) permission from my client to publish it here.
What you'll build
You'll build a simple web application with AngularJS. You'll also add search and edit features with mock data.
What you'll need
- About 15-30 minutes
- A favorite text editor or IDE. I recommend IntelliJ IDEA.
- Git installed.
- Node.js and NPM installed.
Create a simple web application
-
Clone the angular-seed repository using git:
git clone https://github.com/angular/angular-seed.git angular-tutorial cd angular-tutorial
-
There are two kinds of dependencies in this project: tools and angular framework code. The tools help manage and test the application.
- To get the tools that depend upon via
npm
, the node package manager. - To get the angular code via
bower
, a client-side code package manager.
The project has preconfigured
npm
to automatically runbower
so you can simply do:npm install
- To get the tools that depend upon via
Run the application
The project is configured with a simple development web server. The simplest way to start this server is:
npm start
Now browse to the app at http://localhost:8000/app/
.
Add a search feature
To add a search feature, open the project in an IDE or your favorite text editor. For IntelliJ IDEA, use File > New Project > Static Web and point to the directory you cloned angular-seed to.
The Basics
-
Create an
app/search/index.html
file with the following HTML:<form ng-submit="search()"> <input type="search" name="search" ng-model="term"> <button>Search</button> </form>
-
Create
app/search/search.js
and define the routes (URLs) and controller for the search feature.angular.module('myApp.search', ['ngRoute']) .config(['$routeProvider', function ($routeProvider) { $routeProvider .when('/search', { templateUrl: 'search/index.html', controller: 'SearchController' }) }]) .controller('SearchController', function () { console.log("In Search Controller..."); });
-
Modify
app/app.js
and add the "myApp.search" module you created above.angular.module('myApp', [ 'ngRoute', 'myApp.view1', 'myApp.view2', 'myApp.version', 'myApp.search' ])
-
Modify
app/index.html
and add a link to the search.js file.<script src="view2/view2.js"></script> <script src="search/search.js"></script> <script src="components/version/version.js"></script>
-
Refresh your browser and navigate to http://localhost:8000/app/#/search. You should see an input field and search button. You should also see a log message printed in your browser's console. In Chrome, you can view the console using View > Developer > JavaScript Console. You can make it easier to navigate to this page by adding a menu item in
app/index.html
.<ul class="menu"> <li><a href="#/view1">view1</a></li> <li><a href="#/view2">view2</a></li> <li><a href="#/search">search</a></li> </ul>
This section has shown you how to add a new controller and view to a basic AngularJS application. This was fairly simple to create. The next section shows you how to create a fake backend API.
The Backend
To get search results, you're going to create a SearchService that makes HTTP requests. These HTTP requests will be handled by a mock backend using some of Angular's built-in mocking tools. The backend implementation was created using AngularJS Backend-less Development Using a $httpBackend Mock.
-
Add a
SearchService
toapp/search/search.js
. This is done in Angular using its Factory Recipe. Make sure to remove the semicolon from the.controller
code block..controller('SearchController', function () { console.log("In Search Controller..."); }) .factory('SearchService', function ($http) { var service = { query: function (term) { return $http.get('/search/' + term); } }; return service; });
-
Inject the
SearchService
into theSearchController
and use it to get results from the backend. The form inapp/search/index.html
calls thesearch()
function. The "term" is defined by the input field in this page with ng-model="term"..controller('SearchController', function ($scope, SearchService) { $scope.search = function () { console.log("Search term is: " + $scope.term); SearchService.query($scope.term).then(function (response) { $scope.searchResults = response.data; }); }; })
If you try to search for a "foo" term now, you'll see the following error in your console.
Search term is: foo
GET http://localhost:8000/search/foo 404 (Not Found) -
Create
app/search/mock-api.js
for the fake backend. Populate it with the following JavaScript.// We will be using backend-less development // $http uses $httpBackend to make its calls to the server // $resource uses $http, so it uses $httpBackend too // We will mock $httpBackend, capturing routes and returning data angular.module('myApp') .service('ServerDataModel', function ServerDataModel() { this.data = [ { id: 1, name: "Peyton Manning", phone: "(303) 567-8910", address: { street: "1234 Main Street", city: "Greenwood Village", state: "CO", zip: "80111" } }, { id: 2, name: "Demaryius Thomas", phone: "(720) 213-9876", address: { street: "5555 Marion Street", city: "Denver", state: "CO", zip: "80202" } }, { id: 3, name: "Von Miller", phone: "(917) 323-2333", address: { street: "14 Mountain Way", city: "Vail", state: "CO", zip: "81657" } } ]; this.getData = function () { return this.data; }; this.search = function (term) { if (term == "" || term == "*") { return this.getData(); } // find the name that matches the term var list = $.grep(this.getData(), function (element, index) { term = term.toLowerCase(); return (element.name.toLowerCase().match(term)); }); if (list.length === 0) { return []; } else { return list; } }; }) .run(function ($httpBackend, ServerDataModel) { $httpBackend.whenGET(/\/search\/\w+/).respond(function (method, url, data) { // parse the matching URL to pull out the term (/search/:term) var term = url.split('/')[2]; var results = ServerDataModel.search(term); return [200, results, {Location: '/search/' + term}]; }); $httpBackend.whenGET(/search\/index.html/).passThrough(); $httpBackend.whenGET(/view/).passThrough(); });
-
This file uses jQuery.grep(), so you'll need to install jQuery. Modify
bower.json
and add jQuery to the list of dependencies."dependencies": { ... "html5-boilerplate": "~4.3.0", "jquery": "~1.10.x" }
Stop the npm process, run "npm install" to download and install jQuery, then "npm start" again.
-
Modify
app/app.js
and add a dependency on ngMockE2E.angular.module('myApp', ['ngMockE2E', 'ngRoute', ...
-
Modify
app/index.html
to include references tojquery.js
,angular-mocks.js
andmock-api.js
.<script src="bower_components/angular-route/angular-route.js"></script> <script src="bower_components/angular-mocks/angular-mocks.js"></script> <script src="bower_components/jquery/jquery.js"></script> ... <script src="search/mock-api.js"></script> <script src="components/version/version.js"></script>
-
Add the following HTML to
app/search/index.html
to display the search results.<div> <pre>{{ searchResults | json}}</pre> </div>
After making this change, you should be able to search for "p", "d" or "v" and see results as JSON.
-
To make the results more readable, change the above HTML to use a
<table>
and Angular's ng-repeat directive.<table ng-show="searchResults.length" style="width: 100%"> <thead> <tr> <th>Name</th> <th>Phone</th> <th>Address</th> </tr> </thead> <tbody> <tr ng-repeat="person in searchResults"> <td>{{person.name}}</td> <td>{{person.phone}}</td> <td>{{person.address.street}}<br/> {{person.address.city}}, {{person.address.state}} {{person.address.zip}} </td> </tr> </tbody> </table>
This section has shown you how to fetch and display search results. The next section builds on this and shows how to edit and save a record.
Add an Edit Feature
-
Modify
app/search/index.html
to add a link for editing a person.<table ng-show="searchResults.length" style="width: 100%"> ... <tr ng-repeat="person in searchResults"> <td><a href="" ng-click="edit(person)">{{person.name}}</a></td> ... </tr> </table>
-
Add an
edit()
function toSearchController
. Notice that the $location service dependency has been added to the controller's initialization function..controller('SearchController', function ($scope, $location, SearchService) { ... $scope.edit = function (person) { $location.path("/edit/" + person.id); } })
-
Create a route to handle the edit URL in
app/search/search.js
..config(['$routeProvider', function ($routeProvider) { $routeProvider .when('/search', { templateUrl: 'search/index.html', controller: 'SearchController' }) .when('/edit/:id', { templateUrl: 'search/edit.html', controller: 'EditController' }); }])
-
Create
app/search/edit.html
to display an editable form. The HTML below shows how you can use HTML5's data attributes to have valid attributes instead of ng-*.<form ng-submit="save()"> <div> <label for="name">Name:</label> <input type="text" data-ng-model="person.name" id="name"> </div> <div> <label for="phone">Phone:</label> <input type="text" data-ng-model="person.phone" id="phone"> </div> <fieldset> <legend>Address:</legend> <address style="margin-left: 50px"> <input type="text" data-ng-model="person.address.street"><br/> <input type="text" data-ng-model="person.address.city">, <input type="text" data-ng-model="person.address.state" size="2"> <input type="text" data-ng-model="person.address.zip" size="5"> </address> </fieldset> <div> <button type="submit">Save</button> </div> </form>
-
Modify
SearchService
to contain functions for finding a person by their id, and saving them..factory('SearchService', function ($http) { var service = { query: function (term) { return $http.get('/search/' + term); }, fetch: function (id) { return $http.get('/edit/' + id); }, save: function(data) { return $http.post('/edit/' + data.id, data); } }; return service; });
-
Create
EditController
inapp/search/search.js
. $routeParams is an Angular service that allows you to grab parameters from a URL..controller('EditController', function ($scope, $location, $routeParams, SearchService) { SearchService.fetch($routeParams.id).then(function (response) { $scope.person = response.data; }); $scope.save = function() { SearchService.save($scope.person).then(function(response) { $location.path("/search/" + $scope.person.name); }); } })
-
At the bottom of
app/search/mock-api.js
, in itsrun()
function, add the following:$httpBackend.whenGET(/\/search/).respond(function (method, url, data) { var results = ServerDataModel.search(""); return [200, results]; }); $httpBackend.whenGET(/\/edit\/\d+/).respond(function (method, url, data) { // parse the matching URL to pull out the id (/edit/:id) var id = url.split('/')[2]; var results = ServerDataModel.find(id); return [200, results, {Location: '/edit/' + id}]; }); $httpBackend.whenPOST(/\/edit\/\d+/).respond(function(method, url, data) { var params = angular.fromJson(data); // parse the matching URL to pull out the id (/edit/:id) var id = url.split('/')[2]; var person = ServerDataModel.update(id, params); return [201, person, { Location: '/edit/' + id }]; }); $httpBackend.whenGET(/search\/edit.html/).passThrough();
-
Further up in the same file, add
find()
andupdate()
methods toServerDataModel
.this.getData = function () { return this.data; }; this.search = function (term) { ... }; this.find = function (id) { // find the game that matches that id var list = $.grep(this.getData(), function (element, index) { return (element.id == id); }); if (list.length === 0) { return {}; } // even if list contains multiple items, just return first one return list[0]; }; this.update = function (id, dataItem) { // find the game that matches that id var people = this.getData(); var match = null; for (var i = 0; i < people.length; i++) { if (people[i].id == id) { match = people[i]; break; } } if (!angular.isObject(match)) { return {}; } angular.extend(match, dataItem); return match; };
-
The
<form>
inapp/search/edit.html
calls asave()
function to update a person's data. You already implemented this above. The function executes the logic below.$location.path("/search/" + $scope.person.name);
Since the SearchController doesn't execute a search automatically when you execute this URL, add the following logic to do so in
app/search/search.js
. Note that$routeParams
is added to the list of injected dependencies..controller('SearchController', function ($scope, $location, $routeParams, SearchService) { if ($routeParams.term) { SearchService.query($routeParams.term).then(function (response) { $scope.term = $routeParams.term; $scope.searchResults = response.data; }); } ... })
You'll also need to add a new route so search/term is recognized.
.config(['$routeProvider', function ($routeProvider) { $routeProvider .when('/search', { templateUrl: 'search/index.html', controller: 'SearchController' }) .when('/search/:term', { templateUrl: 'search/index.html', controller: 'SearchController' }) ... }])
- After making all these changes, you should be able to refresh your browser and search/edit/update a person's information. If it works - nice job!
Source Code
A completed project with this code in it is available on GitHub at https://github.com/mraible/angular-tutorial.
There are three commits that make the changes for the three main steps in this tutorial:
Extra Credit
Deploy your completed app to Heroku. See running version of this tutorial at https://angular-123.herokuapp.com.
Summary
I hope you've enjoyed this quick-and-easy tutorial on how to get started with AngularJS. In a future tutorial, I'll show you how to write unit tests and integration tests for this application. If you're in Denver next Tuesday, I'll be speaking about AngularJS at Denver's Open Source Users Group.
If you're a Java developer that's interested in developing with AngularJS and Spring Boot, you might want to checkout the recently released JHipster 2.0.