AngularJS example MotoAds more advanced than the tutorial on angularjs.org

I have just been learning AngularJS and decided to build demo application that cover more techniques than the tutorial on angularjs.org. This application is only demo so it do not have complete server side, some features have the todos and there is not any test.

This demo application is some kind of automotive adverts portal, so I called it MotoAds. It has two main features:
  • List of adverts – includes an advanced filtering and sorting.
  • Add advert – a form with some client side field validations.

MotoAds application looks like this: Application folders tree:

Root file index.html included external resource:
  • bootstrap.css (2.3.2)
  • angular.js, angular-resource.js (1.0.8)
  • ui-bootstrap-tpls-0.6.0.js (0.6.0)
and internal resource:
  • app.css
  • app.js
  • services.js
  • controllers.js

Bootstrap angular application, add navigation and route templates

In index.html we auto-bootstrap the angular application by defining directive ng-app: <html ng-app="motoAdsApp"> In app.js we define new angular module motaAdsApp (name must be the same as value of ng-app):
var motoAdsApp = angular.module('motoAdsApp', ['ui.bootstrap', 'motoAdsServices']);
In index.html we use Navbar component from the bootstrap library: <div class="navbar" ng-controller="NavbarController"> <div class="navbar-inner"> <a class="brand" href="#/">MotoAds</a> <ul class="nav"> <li ng-class="{active: routeIs('/')}"><a href="#/">Adverts</a></li> <li ng-class="{active: routeIs('/addAdvert')}"><a href="#/addAdvert">Add advert</a></li> </ul> </div> </div> We define NavbarController with function routeIs (used in ng-class). It allows us to highlight link which is active.
motoAdsApp.controller('NavbarController', function NavbarController($scope, $location) {

  $scope.routeIs = function(routeName) {
    return $location.path() === routeName;
  };

});
We add directive ng-view which renders the proper template into index.html in case of the route definition:
      
Url routes are defined in app.js:
motoAdsApp.config(['$routeProvider',
  function($routeProvider) {
    $routeProvider.
            when('/', {
              controller: 'AdvertsController',
              templateUrl: 'views/adverts.html'
            }).
            when('/addAdvert', {
              controller: 'AddAdvertController',
              templateUrl: 'views/addAdvert.html'
            });
  }]);

Adverts - list, filtering, sorting

In adverts.html we define the left filtering panel with brands and models. We use the ui.bootstrap.accordion component: <accordion close-others="oneAtATime"> <accordion-group heading="{{brand.name}}" ng-repeat="brand in brands"> <ul ng-repeat="model in brand.models"> <li><a href="" ng-click="addBrandModelFilter(brand, model)">{{model.name}}</a></li> </ul> </accordion-group> </accordion> In controller.js we write AdvertsController in which we using method Brand.query() to fill $scope.brands:
motoAdsApp.controller('AdvertsController', ['$scope', 'Brand', 'Country', 'Advert', 
  function($scope, Brand, Country, Advert) {
    $scope.oneAtATime = true;

    $scope.brands = Brand.query();

    // ...
}]);
Definition of the Brand service is in service.js - it is the simple service which reads brands.json:
motoAdsServices.factory('Brand', ['$resource', function($resource) {
    return $resource('data/:id.json', {}, {
      query: {
        method: 'GET',
        params: {
          id: 'brands'
        },
        isArray: true
      }
    });
  }]);
Next in adverts.html we write filters for country, region and year (from-to): <div class="span4"> <label>Country</label> <select ng-model="filter.country" ng-options="c.name for c in countries"> <option value=""></option> </select> </div> <div class="span4"> <label>Region</label> <select ng-model="filter.region" ng-options="r.name for r in filter.country.regions"> <option value=""></option> </select> </div> <div class="span4"> <label>Year of production</label> <input ng-model="filter.yearFrom" class="input-small" min="1980" max="2013" ng-minlength="4" ng-maxlength="4" type="number" placeholder="From"> <input ng-model="filter.yearTo" class="input-small" min="1980" max="2013" ng-minlength="4" ng-maxlength="4" type="number" placeholder="To"> </div> </div> We add to adverts.html the panel with information which filters are active and the options of removing them: <div class="span12"> <span ng-show="isAnyFilter()">Filters:</span> <div class="ma-filter-div"> <span ng-show="filter.modelName">{{filter.brandName}} {{filter.modelName}} <i class="icon-remove-sign icon-white" ng-click="filter.brandName = null; filter.modelName = null"></i></span> <span ng-show="filter.country.name">{{filter.country.name}} <i class="icon-remove-sign icon-white" ng-click="filter.country = null; filter.region = null"></i></span> <span ng-show="filter.region.name">{{filter.region.name}} <i class="icon-remove-sign icon-white" ng-click="filter.region = null"></i></span> <span ng-show="filter.yearFrom">{{filter.yearFrom}} <i class="icon-remove-sign icon-white" ng-click="filter.yearFrom = null"></i></span> <span ng-show="filter.yearTo">{{filter.yearTo}} <i class="icon-remove-sign icon-white" ng-click="filter.yearTo = null"></i></span> </div> <a ng-show="isAnyFilter()" href="" ng-click="removeAllFilter()">Remove all</a> </div> In controller.js we have to fill angular models, define variables and functions which are used by the filters:
motoAdsApp.controller('AdvertsController', ['$scope', 'Brand', 'Country', 'Advert', 
  function($scope, Brand, Country, Advert) {
    $scope.oneAtATime = true;

    $scope.brands = Brand.query();

    $scope.countries = Country.query();

    $scope.sortByCols = [{
        "key": "year",
        "name": "Year"
      }, {
        "key": "price",
        "name": "Price"
      }];

    var allAdverts = Advert.query(filterAdverts);

    $scope.filter = {
      brandName: null,
      modelName: null,
      country: null,
      region: null,
      yearFrom: null,
      yearTo: null
    };

    $scope.isAnyFilter = function() {
      var f = $scope.filter;
      if (f.brandName || f.modelName || f.country || f.region || f.yearFrom || f.yearTo) {
        return true;
      }
      return false;
    };

    $scope.removeAllFilter = function() {
      $scope.filter = {
        brandName: null,
        modelName: null,
        country: null,
        region: null,
        yearFrom: null,
        yearTo: null
      };
    };

    $scope.addBrandModelFilter = function(brand, model) {
      $scope.filter.brandName = brand.name;
      $scope.filter.modelName = model.name;
    };

    $scope.$watch('filter', filterAdverts, true);

    function filterAdverts() {
      $scope.adverts = [];
      angular.forEach(allAdverts, function(row) {
        if (!$scope.filter.country) {
          $scope.filter.region = null;
        }
        if ($scope.filter.brandName && $scope.filter.brandName !== row.brandName) {
          return;
        }
        if ($scope.filter.modelName && $scope.filter.modelName !== row.modelName) {
          return;
        }
        if ($scope.filter.country && $scope.filter.country.name !== row.countryName) {
          return;
        }
        if ($scope.filter.region && $scope.filter.region.name !== row.regionName) {
          return;
        }
        if ($scope.filter.yearFrom && $scope.filter.yearFrom > row.year) {
          return;
        }
        if ($scope.filter.yearTo && $scope.filter.yearTo < row.year) {
          return;
        }
        $scope.adverts.push(row);
      });
    };

  }]);
We define the Country service in service.js:
motoAdsServices.factory('Country', ['$resource', function($resource) {
    return $resource('data/:id.json', {}, {
      query: {
        method: 'GET',
        params: {
          id: 'countries'
        },
        isArray: true
      }
    });
  }]);
Now we add the sorting option and the table containing the list of adverts: <div class="ma-adverts-list"> <div class="row-fluid"> <div class="span4"> <h4>Adverts found ({{adverts.length}})</h4> </div> <div class="offset3 span5"> <form class="form-inline"> <label>Sort by</label> <select ng-model="sortByCol" ng-options="s.name for s in sortByCols"> <option value=""></option> </select> </form> </div> </div> <table class="table table-striped"> <tr> <th>Image/Country/Region</th> <th>Brand</th> <th>Model</th> <th>Year</th> <th>Price ($)</th> </tr> <tr ng-repeat="advert in adverts| orderBy:sortByCol.key"> <td><a href="#/"><img class="img-rounded" ng-src="{{advert.imageUrl}}"></a><br/> <span>{{advert.countryName}}, {{advert.regionName}}</span></td> <td>{{advert.brandName}}</td> <td>{{advert.modelName}}</td> <td>{{advert.year}}</td> <td>{{advert.price}}</td> </tr> </table> </div> The adverts table is filled in controller.js in this lines:
    var allAdverts = Advert.query(filterAdverts);

    function filterAdverts() {
      $scope.adverts = [];
      angular.forEach(allAdverts, function(row) {
        // ...
        $scope.adverts.push(row);
      });
    };
The Advert service is defined in services.js - it reads adverts.json:
motoAdsServices.factory('Advert', ['$resource', function($resource) {
    return $resource('data/:id.json', {}, {
      query: {
        method: 'GET',
        params: {
          id: 'adverts'
        },
        isArray: true
      }
    });
  }]);

Add advert - validation and currency directive



We define the adding advert form in addAdverts.html: <div class="container"> <form name="advertForm" novalidate class="ma-add-advert-form form-horizontal"> <h4>Add new advert</h4> <div class="control-group"> <label class="control-label">Brand</label> <div class="controls"> <select name="brand" ng-model="newAdvert.brand" required ng-options="c.name for c in brands"> <option value=""></option> </select> <span class="ma-form-msg-error help-inline" ng-show="advertForm.brand.$dirty && advertForm.brand.$invalid">Invalid brand</span> </div> </div> <div class="control-group"> <label class="control-label">Model</label> <div class="controls"> <select name="model" ng-model="newAdvert.model" required ng-options="r.name for r in newAdvert.brand.models"> <option value=""></option> </select> <span class="ma-form-msg-error help-inline" ng-show="advertForm.model.$dirty && advertForm.model.$invalid">Invalid model</span> </div> </div> <div class="control-group"> <label class="control-label">Year</label> <div class="controls"> <input type="number" name="year" ng-model="newAdvert.year" required min="1900" ng-minlength="4" ng-maxlength="4"> <span class="ma-form-msg-error help-inline" ng-show="advertForm.year.$dirty && advertForm.year.$invalid">Invalid year</span> </div> </div> <div class="control-group"> <label class="control-label">Price ($)</label> <div class="controls"> <input type="text" ma-currency name="price" ng-model="newAdvert.price" required> <span class="ma-form-msg-error help-inline" ng-show="advertForm.price.$dirty && advertForm.price.$invalid">Invalid price</span> </div> </div> <div class="control-group"> <label class="control-label">Country</label> <div class="controls"> <select name="country" ng-model="newAdvert.country" required ng-options="c.name for c in countries"> <option value=""></option> </select> <span class="ma-form-msg-error help-inline" ng-show="advertForm.country.$dirty && advertForm.country.$invalid">Invalid country</span> </div> </div> <div class="control-group"> <label class="control-label">Region</label> <div class="controls"> <select name="region" ng-model="newAdvert.region" required ng-options="r.name for r in newAdvert.country.regions"> <option value=""></option> </select> <span class="ma-form-msg-error help-inline" ng-show="advertForm.region.$dirty && advertForm.region.$invalid">Invalid region</span> </div> </div> <div class="control-group"> <div class="controls"> <button ng-click="reset()" ng-disabled="isUnchanged()" class="btn btn-primary">Reset</button> <button ng-click="add()" ng-disabled="advertForm.$invalid || isUnchanged()" class="btn btn-primary">Save</button> </div> </div> <div class="muted">Notice! Images upload not supported yet.</div> </form> </div> We write AddAdvertController in controllers.js:
motoAdsApp.controller('AddAdvertController', ['$scope', 'Brand', 'Country', 'Advert',
  function($scope, Brand, Country) {
    $scope.brands = Brand.query();

    $scope.countries = Country.query();

    $scope.emptyAdvert = {
      brand: null,
      model: null,
      year: 2010,
      price: 10000,
      country: null,
      region: null
    };

    $scope.add = function() {
      alert('User added!');
      // TODO: Store it!
      $scope.reset();
    };

    $scope.reset = function() {
      $scope.newAdvert = angular.copy($scope.emptyAdvert);
      if ($scope.advertForm) {
        // TODO Uncomment in angular 1.1.1 or higher
        //$scope.advertForm.$setPristne();
      }
    };

    $scope.isUnchanged = function() {
      return angular.equals($scope.newAdvert, $scope.emptyAdvert);
    };

    $scope.reset();
  }]);
Last thing is custom directive ma-currency used in addAdvert.html: