As you know, we are developing the SPA. Therefore, once the application loads, you can perform all the operations without page refresh. All interactions with the server are performed using AJAX calls. Now, we'll make use of the AngularJS concepts that we have covered in the first section. We'll cover the following scenarios:
For the home page, we will create index.html
and a template that will contain the restaurant listing in the middle section or the content area.
The home page is the main page of any web application. To design the home page, we are going to use the Angular-UI bootstrap rather than the actual bootstrap. Angular-UI is an Angular version of the bootstrap. The home page will be divided into three sections:
You must be interested in viewing the home page before designing or implementing it. Therefore, let us first see how it will look like once we have our content ready:
Now, to design our home page, we need to add following four files:
First, we'll add the ./app/index.html
in our project workspace. The contents of index.html
will be as explained here onwards.
index.html
is divided into many parts. We'll discuss a few of the key parts here. First, we will see how to address old Internet Explorer versions. If you want to target the Internet Explorer browser versions greater than 8 or IE version 9 onwards, then we need to add following block that will prevent JavaScript rendering and give the no-js output to the end-user.
<!--[if lt IE 7]> <html lang="en" ng-app="otrsApp" class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]--> <!--[if IE 7]> <html lang="en" ng-app="otrsApp" class="no-js lt-ie9 lt-ie8"> <![endif]--> <!--[if IE 8]> <html lang="en" ng-app="otrsApp" class="no-js lt-ie9"> <![endif]--> <!--[if gt IE 8]><!--> <html lang="en" ng-app="otrsApp" class="no-js"> <!--<![endif]-->
Then, after adding a few meta tags and the title of the application, we'll also define the important meta tag
viewport. The viewport is used for responsive UI designs.
The width
property defined in the content attribute controls the size of the viewport. It can be set to a specific number of pixels like width = 600
or to the special value device-width value which is the width of the screen in CSS pixels at a scale of 100%.
The initial-scale property controls the zoom level when the page is first loaded. The maximum-scale, minimum-scale, and user-scalable properties control how users are allowed to zoom the page in or out.
<meta name="viewport" content="width=device-width, initial-scale=1">
In the next few lines, we'll define the style sheets of our application. We are adding normalize.css
and main.css
from HTML5 boilerplate code. We are also adding our application's customer CSS app.css
. Finally, we are adding the bootstrap 3 CSS. Apart from the customer app.css
, other CSS are referenced in it. There is no change in these CSS files.
<link rel="stylesheet" href="bower_components/html5-boilerplate/dist/css/normalize.css"> <link rel="stylesheet" href="bower_components/html5-boilerplate/dist/css/main.css"> <link rel="stylesheet" href="public/css/app.css"> <link data-require="bootstrap-css@*" data-server="3.0.0" rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" />
Then we'll define the scripts using the script
tag. We are adding the modernizer, Angular, Angular-route, and our own developed custom JavaScript file app.js
. We have already discussed Angular and Angular-UI. app.js
will be discussed in the next section.
Modernizer allows web developers to use new CSS3 and HTML5 features while maintaining a fine level of control over browsers that don't support them. Basically, modernizer performs the next generation feature detection (checking the availability of those features) while the page loads in the browser and reports the results. Based on these results you can detect what are the latest features available in the browser and based on that you can provide an interface to the end user. If the browser does not support a few of the features then an alternate flow or UI is provided to the end user.
We are also adding the bootstrap templates which are written in JavaScript using the ui-bootstrap-tpls javascript
file.
<script src="bower_components/html5-boilerplate/dist/js/vendor/modernizr-2.8.3.min.js"></script> <script src="bower_components/angular/angular.min.js"></script> <script src="bower_components/angular-route/angular-route.min.js"></script> <script src="app.js"></script> <script data-require="[email protected]" data-semver="0.5.0" src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.6.0.js"></script>
We can also add style to the head
tag as shown in the following. This style allows drop-down menus to work.
<style> div.navbar-collapse.collapse { display: block; overflow: hidden; max-height: 0px; -webkit-transition: max-height .3s ease; -moz-transition: max-height .3s ease; -o-transition: max-height .3s ease; transition: max-height .3s ease; } div.navbar-collapse.collapse.in { max-height: 2000px; } </style>
In the body
tag we are defining the controller of the application using the ng-controller
attribute. While the page loads, it tells the controller the name of the application to Angular.
<body ng-controller="otrsAppCtrl">
Then, we define the header
section of the home page. In the header
section, we'll define the application title, Online Table Reservation System
. Also, we'll define the search form that will search the restaurants.
<!-- BEGIN HEADER --> <nav class="navbar navbar-default" role="navigation"> <div class="navbar-header"> <a class="navbar-brand" href="#"> Online Table Reservation System </a> </div> <div class="collapse navbar-collapse" ng-class="!navCollapsed && 'in'" ng-click="navCollapsed = true"> <form class="navbar-form navbar-left" role="search" ng-submit="search()"> <div class="form-group"> <input type="text" id="searchedValue" ng-model="searchedValue" class="form-control" placeholder="Search Restaurants"> </div> <button type="submit" class="btn btn-default" ng-click="">Go</button> </form> <!-- END HEADER -->
Then, in the next section, the middle section, includes where we actually bind the different views, marked with actual content comments. The ui-view
attribute in div
gets its content dynamically from Angular such as restaurant details, restaurant list, and so on. We have also added a warning dialog and spinner to the middle section that will be visible as and when required.
<div class="clearfix"></div> <!-- BEGIN CONTAINER --> <div class="page-container container"> <!-- BEGIN CONTENT --> <div class="page-content-wrapper"> <div class="page-content"> <!-- BEGIN ACTUAL CONTENT --> <div ui-view class="fade-in-up"></div> <!-- END ACTUAL CONTENT --> </div> </div> <!-- END CONTENT --> </div> <!-- loading spinner --> <div id="loadingSpinnerId" ng-show="isSpinnerShown()" style="top:0; left:45%; position:absolute; z-index:999"> <script type="text/ng-template" id="alert.html"> <div class="alert alert-warning" role="alert"> <div ng-transclude></div> </div> </script> <uib-alert type="warning" template-url="alert.html"><b>Loading...</b></uib-alert> </div> <!-- END CONTAINER -->
The final section of the index.html
is the footer. Here, we are just adding the static content and copyright text. You can add whatever content you want here.
<!-- BEGIN FOOTER --> <div class="page-footer"> <hr/><div style="padding: 0 39%">© 2016 Online Table Reservation System</div> </div> <!-- END FOOTER --> </body> </html>
app.js
is our main application file. Because we have defined it in index.html
, it gets loaded as soon as our index.html
is called.
As we are using the Edge Server (Proxy Server), everything will be accessible from it including our REST endpoints. External applications including the UI will use the Edge Server host to access the application. You can configure it in some global constants file and then use it wherever it is required. This will allow you to configure the REST host at a single place and use it at other places.
'use strict'; /* This call initializes our application and registers all the modules, which are passed as an array in the second argument. */ var otrsApp = angular.module('otrsApp', [ 'ui.router', 'templates', 'ui.bootstrap', 'ngStorage', 'otrsApp.httperror', 'otrsApp.login', 'otrsApp.restaurants' ]) /* Then we have defined the default route /restaurants */ .config([ '$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) { $urlRouterProvider.otherwise('/restaurants'); }]) /* This functions controls the flow of the application and handles the events. */ .controller('otrsAppCtrl', function ($scope, $injector, restaurantService) { var controller = this; var AjaxHandler = $injector.get('AjaxHandler'); var $rootScope = $injector.get('$rootScope'); var log = $injector.get('$log'); var sessionStorage = $injector.get('$sessionStorage'); $scope.showSpinner = false; /* This function gets called when the user searches any restaurant. It uses the Angular restaurant service that we'll define in the next section to search the given search string. */ $scope.search = function () { $scope.restaurantService = restaurantService; restaurantService.async().then(function () { $scope.restaurants = restaurantService.search($scope.searchedValue); }); } /* When the state is changed, the new controller controls the flows based on the view and configuration and the existing controller is destroyed. This function gets a call on the destroy event. */ $scope.$on('$destroy', function destroyed() { log.debug('otrsAppCtrl destroyed'); controller = null; $scope = null; }); $rootScope.fromState; $rootScope.fromStateParams; $rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromStateParams) { $rootScope.fromState = fromState; $rootScope.fromStateParams = fromStateParams; }); // utility method $scope.isLoggedIn = function () { if (sessionStorage.session) { return true; } else { return false; } }; /* spinner status */ $scope.isSpinnerShown = function () { return AjaxHandler.getSpinnerStatus(); }; }) /* This function gets executed when this object loads. Here we are setting the user object which is defined for the root scope. */ .run(['$rootScope', '$injector', '$state', function ($rootScope, $injector, $state) { $rootScope.restaurants = null; // self reference var controller = this; // inject external references var log = $injector.get('$log'); var $sessionStorage = $injector.get('$sessionStorage'); var AjaxHandler = $injector.get('AjaxHandler'); if (sessionStorage.currentUser) { $rootScope.currentUser = $sessionStorage.currentUser; } else { $rootScope.currentUser = "Guest"; $sessionStorage.currentUser = "" } }])
restaurants.js
represents an Angular service for our app which we'll use for the restaurants. We know that there are two common uses of services – organizing code and sharing code across apps. Therefore, we have created a restaurants service which will be used among different modules like search, list, details, and so on.
The following section initializes the restaurant service module and loads the required dependencies.
angular.module('otrsApp.restaurants', [ 'ui.router', 'ui.bootstrap', 'ngStorage', 'ngResource' ])
In the configuration, we are defining the routes and state of the otrsApp.restaurants
module using UI-Router.
First we define the restaurants
state by passing the JSON object containing the URL that points the router URI, the template URL that points to the HTML template that display the restaurants
state, and the controller that will handle the events on the restaurants
view.
On top of the restaurants
view (route - /restaurants
), a nested state restaurants.profile
is also defined that will represent the specific restaurant. For example, /restaurant/1
would open and display the restaurant profile (details) page of a restaurant which is represented by Id 1
. This state is called when a link is clicked in the restaurants
template. In this ui-sref="restaurants.profile({id: rest.id})"
rest represents the restaurant
object retrieved from the restaurants
view.
Notice that the state name is 'restaurants.profile'
which tells the AngularJS UI Router that the profile is a nested state of the restaurants
state.
.config([ '$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) { $stateProvider.state('restaurants', { url: '/restaurants', templateUrl: 'restaurants/restaurants.html', controller: 'RestaurantsCtrl' }) // Restaurant show page .state('restaurants.profile', { url: '/:id', views: { '@': { templateUrl: 'restaurants/restaurant.html', controller: 'RestaurantCtrl' } } }); }])
In the next code section, we are defining the restaurant service using the Angular factory service type. This restaurant service on load fetches the list of restaurants from the server using a REST call. It provides a list and searches restaurant operations and restaurant data.
.factory('restaurantService', function ($injector, $q) { var log = $injector.get('$log'); var ajaxHandler = $injector.get('AjaxHandler'); var deffered = $q.defer(); var restaurantService = {}; restaurantService.restaurants = []; restaurantService.orignalRestaurants = []; restaurantService.async = function () { ajaxHandler.startSpinner(); if (restaurantService.restaurants.length === 0) { ajaxHandler.get('/api/restaurant') .success(function (data, status, headers, config) { log.debug('Getting restaurants'); sessionStorage.apiActive = true; log.debug("if Restaurants --> " + restaurantService.restaurants.length); restaurantService.restaurants = data; ajaxHandler.stopSpinner(); deffered.resolve(); }) .error(function (error, status, headers, config) { restaurantService.restaurants = mockdata; ajaxHandler.stopSpinner(); deffered.resolve(); }); return deffered.promise; } else { deffered.resolve(); ajaxHandler.stopSpinner(); return deffered.promise; } }; restaurantService.list = function () { return restaurantService.restaurants; }; restaurantService.add = function () { console.log("called add"); restaurantService.restaurants.push( { id: 103, name: 'Chi Cha's Noodles', address: '13 W. St., Eastern Park, New County, Paris', }); }; restaurantService.search = function (searchedValue) { ajaxHandler.startSpinner(); if (!searchedValue) { if (restaurantService.orignalRestaurants.length > 0) { restaurantService.restaurants = restaurantService.orignalRestaurants; } deffered.resolve(); ajaxHandler.stopSpinner(); return deffered.promise; } else { ajaxHandler.get('/api/restaurant?name=' + searchedValue) .success(function (data, status, headers, config) { log.debug('Getting restaurants'); sessionStorage.apiActive = true; log.debug("if Restaurants --> " + restaurantService.restaurants.length); if (restaurantService.orignalRestaurants.length < 1) { restaurantService.orignalRestaurants = restaurantService.restaurants; } restaurantService.restaurants = data; ajaxHandler.stopSpinner(); deffered.resolve(); }) .error(function (error, status, headers, config) { if (restaurantService.orignalRestaurants.length < 1) { restaurantService.orignalRestaurants = restaurantService.restaurants; } restaurantService.restaurants = []; restaurantService.restaurants.push( { id: 104, name: 'Gibsons - Chicago Rush St.', address: '1028 N. Rush St., Rush & Division, Cook County, Paris' }); restaurantService.restaurants.push( { id: 105, name: 'Harry Caray's Italian Steakhouse', address: '33 W. Kinzie St., River North, Cook County, Paris', }); ajaxHandler.stopSpinner(); deffered.resolve(); }); return deffered.promise; } }; return restaurantService; })
In the next section of the restaurants.js
module, we'll add two controllers that we defined for the restaurants and restaurants.profile
states in the routing configuration. These two controllers are RestaurantsCtrl
and RestaurantCtrl
that handle the restaurants
state and the restaurants.profiles
states respectively.
RestaurantsCtrl
is pretty simple in that it loads the restaurants data using the restaurants service list method.
.controller('RestaurantsCtrl', function ($scope, restaurantService) { $scope.restaurantService = restaurantService; restaurantService.async().then(function () { $scope.restaurants = restaurantService.list(); }); })
RestaurantCtrl
is responsible for showing the restaurant details of a given ID. This is also responsible for performing the reservation operations on the displayed restaurant. This control will be used when we design the restaurant details page with reservation options.
.controller('RestaurantCtrl', function ($scope, $state, $stateParams, $injector, restaurantService) { var $sessionStorage = $injector.get('$sessionStorage'); $scope.format = 'dd MMMM yyyy'; $scope.today = $scope.dt = new Date(); $scope.dateOptions = { formatYear: 'yy', maxDate: new Date().setDate($scope.today.getDate() + 180), minDate: $scope.today.getDate(), startingDay: 1 }; $scope.popup1 = { opened: false }; $scope.altInputFormats = ['M!/d!/yyyy']; $scope.open1 = function () { $scope.popup1.opened = true; }; $scope.hstep = 1; $scope.mstep = 30; if ($sessionStorage.reservationData) { $scope.restaurant = $sessionStorage.reservationData.restaurant; $scope.dt = new Date($sessionStorage.reservationData.tm); $scope.tm = $scope.dt; } else { $scope.dt.setDate($scope.today.getDate() + 1); $scope.tm = $scope.dt; $scope.tm.setHours(19); $scope.tm.setMinutes(30); restaurantService.async().then(function () { angular.forEach(restaurantService.list(), function (value, key) { if (value.id === parseInt($stateParams.id)) { $scope.restaurant = value; } }); }); } $scope.book = function () { var tempHour = $scope.tm.getHours(); var tempMinute = $scope.tm.getMinutes(); $scope.tm = $scope.dt; $scope.tm.setHours(tempHour); $scope.tm.setMinutes(tempMinute); if ($sessionStorage.currentUser) { console.log("$scope.tm --> " + $scope.tm); alert("Booking Confirmed!!!"); $sessionStorage.reservationData = null; $state.go("restaurants"); } else { $sessionStorage.reservationData = {}; $sessionStorage.reservationData.restaurant = $scope.restaurant; $sessionStorage.reservationData.tm = $scope.tm; $state.go("login"); } } })
We have also added a few of the filters in the restaurants.js
module to format the date and time. These filters perform the following formatting on the input data:
date1
: Returns the input date in 'dd MMM yyyy' format, for example 13-Apr-2016time1
: Returns the input time in 'HH:mm:ss' format, for example 11:55:04dateTime1
: Returns the input date and time in 'dd MMM yyyy HH:mm:ss' format, for example 13-Apr-2016 11:55:04In the following code snippet we've applied these three filters:
.filter('date1', function ($filter) { return function (argDate) { if (argDate) { var d = $filter('date')(new Date(argDate), 'dd MMM yyyy'); return d.toString(); } return ""; }; }) .filter('time1', function ($filter) { return function (argTime) { if (argTime) { return $filter('date')(new Date(argTime), 'HH:mm:ss'); } return ""; }; }) .filter('datetime1', function ($filter) { return function (argDateTime) { if (argDateTime) { return $filter('date')(new Date(argDateTime), 'dd MMM yyyy HH:mm a'); } return ""; }; });
We need to add the templates that we have defined for the restaurants.profile
state. As you can see in the template we are using the ng-repeat
directive to iterate the list of objects returned by restaurantService.restaurants
. The restaurantService
scope variable is defined in the controller. 'RestaurantsCtrl'
is associated with this template in the restaurants state.
<h3>Famous Gourmet Restaurants in Paris</h3> <div class="row"> <div class="col-md-12"> <table class="table table-bordered table-striped"> <thead> <tr> <th>#Id</th> <th>Name</th> <th>Address</th> </tr> </thead> <tbody> <tr ng-repeat="rest in restaurantService.restaurants"> <td>{{rest.id}}</td> <td><a ui-sref="restaurants.profile({id: rest.id})">{{rest.name}}</a></td> <td>{{rest.address}}</td> </tr> </tbody> </table> </div> </div>
On the home page index.html
we have added the search form in the header
section that allows us to search restaurants. The Search Restaurants functionality will use the same files as described earlier. It makes use of the app.js
(search form handler), restaurants.js
(restaurant service), and restaurants.html
to display the searched records.
Restaurant details with reservation option will be the part of the content area (middle section of the page). This will contain a breadcrumb at the top with restaurants as a link to the restaurant listing page, followed by the name and address of the restaurant. The last section will contain the reservation section containing date time selection boxes and reserve button.
This page will look like the following screenshot:
Here, we will make use of the same restaurant service declared in restaurants.js
. The only change will be the template as described for the state restaurants.profile
. This template will be defined using the restaurant.html
.
As you can see, the breadcrumb is using the restaurants route, which is defined using the ui-sref
attribute. The reservation form designed in this template calls the book()
function defined in the controller RestaurantCtrl
using the directive ng-submit
on the form submit.
<div class="row"> <div class="row"> <div class="col-md-12"> <ol class="breadcrumb"> <li><a ui-sref="restaurants">Restaurants</a></li> <li class="active">{{restaurant.name}}</li> </ol> <div class="bs-docs-section"> <h1 class="page-header">{{restaurant.name}}</h1> <div> <strong>Address:</strong> {{restaurant.address}} </div> </br></br> <form ng-submit="book()"> <div class="input-append date form_datetime"> <div class="row"> <div class="col-md-7"> <p class="input-group"> <span style="display: table-cell; vertical-align: middle; font-weight: bolder; font-size: 1.2em">Select Date & Time for Booking:</span> <span style="display: table-cell; vertical-align: middle"> <input type="text" size=20 class="form-control" uib-datepicker-popup="{{format}}" ng-model="dt" is-open="popup1.opened" datepicker-options="dateOptions" ng-required="true" close-text="Close" alt-input-formats="altInputFormats" /> </span> <span class="input-group-btn"> <button type="button" class="btn btn-default" ng-click="open1()"><i class="glyphicon glyphicon-calendar"></i></button> </span> <uib-timepicker ng-model="tm" ng-change="changed()" hour-step="hstep" minute-step="mstep"></uib-timepicker> </p> </div> </div></div> <div class="form-group"> <button class="btn btn-primary" type="submit">Reserve</button> </div> </form></br></br> </div> </div> </div>
When a user clicks on the Reserve button on the Restaurant Detail page after selecting the date and time of the reservation, the Restaurant Detail page checks whether the user is already logged in or not. If the user is not logged in, then the Login page displays. It looks like the following screenshot:
Once the user logs in, the user is redirected back to same booking page with the persisted state. Then the user can proceed with the reservation. The Login page uses basically two files: login.html
and login.js
.
The login.html
template consists of only two input fields, username and password, with the Login button and Cancel link. The Cancel link resets the form and the Login button submits the login form.
Here, we are using the LoginCtrl
with the ng-controller
directive. The Login form is submitted using the ng-submit
directive that calls the submit
function of LoginCtrl
. Input values are first collected using the ng-model
directive and then submitted using their respective properties - _email
and _password
.
<div ng-controller="LoginCtrl as loginC" style="max-width: 300px"> <h3>Login</h3> <div class="form-container"> <form ng-submit="loginC.submit(_email, _password)"> <div class="form-group"> <label for="username" class="sr-only">Username</label> <input type="text" id="username" class="form-control" placeholder="username" ng-model="_email" required autofocus /> </div> <div class="form-group"> <label for="password" class="sr-only">Password</label> <input type="password" id="password" class="form-control" placeholder="password" ng-model="_password" /> </div> <div class="form-group"> <button class="btn btn-primary" type="submit">Login</button> <button class="btn btn-link" ng-click="loginC.cancel()">Cancel</button> </div> </form> </div> </div>
The login module is defined in the login.js
that contains and loads the dependencies using the module function. The state login is defined with the help of the config function that takes the JSON object containing the url, controller, and templateUrl
properties.
Inside the controller, we define the cancel and submit operations, which are called from the login.html
template.
angular.module('otrsApp.login', [ 'ui.router', 'ngStorage' ]) .config(function config($stateProvider) { $stateProvider.state('login', { url: '/login', controller: 'LoginCtrl', templateUrl: 'login/login.html' }); }) .controller('LoginCtrl', function ($state, $scope, $rootScope, $injector) { var $sessionStorage = $injector.get('$sessionStorage'); if ($sessionStorage.currentUser) { $state.go($rootScope.fromState.name, $rootScope.fromStateParams); } var controller = this; var log = $injector.get('$log'); var http = $injector.get('$http'); $scope.$on('$destroy', function destroyed() { log.debug('LoginCtrl destroyed'); controller = null; $scope = null; }); this.cancel = function () { $scope.$dismiss; $state.go('restaurants'); } console.log("Current --> " + $state.current); this.submit = function (username, password) { $rootScope.currentUser = username; $sessionStorage.currentUser = username; if ($rootScope.fromState.name) { $state.go($rootScope.fromState.name, $rootScope.fromStateParams); } else { $state.go("restaurants"); } }; });