Matt RaibleMatt Raible is a Web Architecture Consultant specializing in open source frameworks.

The JHipster Mini-Book The JHipster Mini-Book is a guide to getting started with hip technologies today: AngularJS, Bootstrap, and Spring Boot. All of these frameworks are wrapped up in an easy-to-use project called JHipster.

This book shows you how to build an app with JHipster, and guides you through the plethora of tools, techniques and options you can use. Furthermore, it explains the UI and API building blocks so you understand the underpinnings of your great application.

For book updates, follow @jhipster-book on Twitter.

10+ YEARS


Over 10 years ago, I wrote my first blog post. Since then, I've authored books, had kids, traveled the world, found Trish and blogged about it all.

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

  1. Clone the angular-seed repository using git:

    git clone https://github.com/angular/angular-seed.git angular-tutorial
    cd angular-tutorial
  2. There are two kinds of dependencies in this project: tools and angular framework code. The tools help manage and test the application.

    The project has preconfigured npm to automatically run bower so you can simply do:

    npm install

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

  1. 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>
    
  2. 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...");
        });
    
  3. 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'
    ])
  4. 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>
    
  5. 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.

  1. Add a SearchService to app/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;
    });
    
  2. Inject the SearchService into the SearchController and use it to get results from the backend. The form in app/search/index.html calls the search() 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)
  3. 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();
        });
  4. 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.

  5. Modify app/app.js and add a dependency on ngMockE2E.

    angular.module('myApp', ['ngMockE2E',
      'ngRoute',
      ...
  6. Modify app/index.html to include references to jquery.js, angular-mocks.js and mock-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>
  7. 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.

  8. 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

  1. 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>
    
  2. Add an edit() function to SearchController. 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);
        }
    })
  3. 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'
            });
    }])
  4. 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>
    
  5. 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;
    });
  6. Create EditController in app/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);
            });
        }
    })
  7. At the bottom of app/search/mock-api.js, in its run() 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();
  8. Further up in the same file, add find() and update() methods to ServerDataModel.

    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;
    };
  9. The <form> in app/search/edit.html calls a save() 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'
            })
            ...
    }])
  10. 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.

Posted in The Web at Jan 29 2015, 11:12:38 AM MST Add a Comment
Comments:

Post a Comment:
  • HTML Syntax: Allowed