<!DOCTYPE html>
<html ng-app="app">
<head>
<meta charset="UTF-8" />
<link data-require="bootstrap-css@*" data-semver="3.2.0" rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" />
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.17/angular.js" data-semver="1.2.17" data-require="angular.js@1.2.17"></script>
<script src="https://code.angularjs.org/1.2.17/angular-mocks.js" data-semver="1.2.17" data-require="angular-mocks@1.2.17"></script>
<script data-require="angular-resource@1.2.17" data-semver="1.2.17" src="http://code.angularjs.org/1.2.17/angular-resource.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.js" data-semver="1.11.0" data-require="jquery@1.11.0"></script>
<script data-require="angular-ui-bootstrap@0.11.0" data-semver="0.11.0" src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.11.0.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="app.js"></script>
<script src="app-mockbackend.js"></script>
<script src="services/ServerDataModel.js"></script>
<script src="services/HttpService.js"></script>
<script src="services/ResourceService.js"></script>
<script src="app-controller.js"></script>
</head>
<body ng-controller="Ctrl" class="container-fluid">
<h1>AngularJS backend-less development by mocking $httpBackend</h1>
<tabset justified="true">
<tab heading="Create">
<div ng-include="'templates/create.html'"></div>
</tab>
<tab heading="Read All - query()">
<div ng-include="'templates/query.html'"></div>
</tab>
<tab heading="Read One - get()">
<div ng-include="'templates/get.html'"></div>
</tab>
<tab heading="Update">
<div ng-include="'templates/update.html'"></div>
</tab>
<tab heading="Delete">
<div ng-include="'templates/delete.html'"></div>
</tab>
</tabset>
<h4>Current Server Data:</h4>
<div class="row">
<div class="col-md-6">
<pre>{{ dataModel.data | json }}</pre>
</div>
</div>
</body>
</html>
.nav, .pagination, .carousel, .panel-title a { cursor: pointer; }
# AngularJS backend-less development by mocking $httpBackend
The purpose of this example code is to show how to do backend-less development with code that uses both $http and $resource to fully cover the most common server communication options within AngularJS.
## Why do backend-less development?
Isolating your AngularJS frontend development from the backend (REST API) can allow potentially separate teams develop independently. Agreeing on the REST API can then allow both teams develop in parallel, agreeing to implement/use the REST service the same way.
This can be accomplished by the simple inclusion of a file in your AngularJS code that creates a mock using $httpBackend. $httpBackend makes the requests that are underlying $http, which is also what ngResource $resource uses. When you are ready to hit the real backend REST API, simple remove the file from being included, possibly as simple as a special task inside your grunt config.
There are two different flavors of the $httpBackend mock, we want to use the one not for unit testing, but for E2E testing:
[AngularJS $httpBackend docs](https://docs.angularjs.org/api/ngMockE2E/service/$httpBackend)
## How do we do it?
We use URL patterns within **app-mockbackend.js** to intercept the GET and POST calls to the URLs, along with the data for a POST. We can use regular expressions as patterns, which allows for a lot of flexibility.
The handling of the URLs and HTTP methods and returning "fake" data only works by having some data that can be persistent from request to request. I store the data in an AngularJS service **ServerDataModel** that emulates storing the data a lot like the server would. The AngularJS service recipe is perfect for this becuase it injects a constructed object, and that object contains our data. No matter where it is injected, the instance of the object is what is shared so it contains the same data. There is some data that is pre-loaded in that object that is analagous to having a database on the server that already has some records.
angular.module('app', ['ngMockE2E', 'ngResource', 'ui.bootstrap']);
angular.module('app').controller('Ctrl', function($scope, ResourceService, HttpService, ServerDataModel) {
// putting our server data on scope to display it for learning purposes
$scope.dataModel = ServerDataModel;
// 1) Add new game form
$scope.masterGame = {
date: new Date()
};
$scope.reset = function() {
$scope.game = angular.copy($scope.masterGame);
};
$scope.saveHttp = function(game) {
HttpService.save(game);
};
$scope.saveResource = function(game) {
ResourceService.save(game);
};
$scope.reset();
// date field display options
$scope.opened = {};
$scope.open = function($event, id) {
$event.preventDefault();
$event.stopPropagation();
$scope.opened[id] = true;
};
$scope.dateOptions = {
formatYear: 'yy',
startingDay: 1
};
// 2) Query data
$scope.queryReset = function() {
$scope.queryResults = [];
};
$scope.queryHttp = function() {
HttpService.query().then(function(response) {
$scope.queryResults = response.data;
});
};
$scope.queryResource = function() {
$scope.queryResults = ResourceService.query();
};
$scope.queryReset();
// 3) Get single record
$scope.getReset = function() {
$scope.getGameid = 1;
$scope.getResults = null;
};
$scope.getHttp = function(gameid) {
HttpService.get(gameid).then(function(response) {
$scope.getResults = response.data;
});
};
$scope.getResource = function(gameid) {
$scope.getResults = ResourceService.get({gameid: gameid});
};
// 4) Update a single record
$scope.updateReset = function() {
// updateGameid is used for the initial record to select
$scope.updateGameid = 1;
// updateGame holds the game fetched from server using updateGameId
$scope.updateGame = null;
// updateResults holds the results of query() when update has completed
$scope.updateResults = [];
};
$scope.updateGetResource = function(gameid) {
$scope.updateGame = ResourceService.get({gameid: gameid});
};
$scope.updateHttp = function(game) {
HttpService.save(game).then(function(response) {
// ignore the response, just query to pull in everything
HttpService.query().then(function(queryResponse) {
$scope.updateResults = queryResponse.data;
});
});
};
// because the game object was from updateGetResource(), it is already a
// $resource object, so can just call $save on it
$scope.updateResource = function(game) {
$scope.updateGame.$save().then(function(response) {
$scope.updateResults = ResourceService.query();
});
};
$scope.updateReset();
// 5) Delete a single record
$scope.deleteReset = function() {
// deleteGameid is used for the initial record to select
$scope.deleteGameid = 1;
// deleteGame holds the game fetched from server using deleteGameId
$scope.deleteGame = null;
// deleteResults holds the results of query() when delete has completed
$scope.deleteResults = [];
};
$scope.deleteGetResource = function(gameid) {
$scope.deleteGame = ResourceService.get({gameid: gameid});
};
$scope.deleteHttp = function(gameid) {
HttpService.delete(gameid).then(function(response) {
// ignore the response, just query to pull in everything
HttpService.query().then(function(queryResponse) {
$scope.deleteResults = queryResponse.data;
});
});
};
// because the game object was from deleteGetResource(), it is already a
// $resource object, so can just call $delete on it
$scope.deleteResource = function(gameid) {
$scope.deleteGame = ResourceService.get({gameid: gameid}, function() {
if(!angular.isDefined($scope.deleteGame.gameid)) { return; }
$scope.deleteGame.$delete().then(function() {
$scope.deleteResults = ResourceService.query();
});
});
};
$scope.deleteReset();
});
angular.module('app').factory('HttpService', function($http) {
var service = {
query: function() {
return $http.get('/games');
},
get: function(id) {
return $http.get('/games/' + id);
},
// making save dual-function like default ngResource behavior (no separate update w/PUT)
save: function(data) {
if(angular.isDefined(data.gameid)) {
return $http.post('/games/' + data.gameid, data);
} else {
return $http.post('/games', data);
}
},
delete: function(id) {
return $http.delete('/games/' + id);
}
};
return service;
})
angular.module('app').factory('ResourceService', function($resource) {
return $resource('/games/:gameid', {gameid: '@gameid'});
});
// 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('app').run(function($httpBackend, ServerDataModel) {
$httpBackend.whenGET('/games').respond(function(method, url, data) {
var games = ServerDataModel.findAll();
return [200, games, {}];
});
$httpBackend.whenGET(/\/games\/\d+/).respond(function(method, url, data) {
// parse the matching URL to pull out the id (/games/:id)
var gameid = url.split('/')[2];
var game = ServerDataModel.findOne(gameid);
return [200, game, {}];
});
// this is the creation of a new resource
$httpBackend.whenPOST('/games').respond(function(method, url, data) {
var params = angular.fromJson(data);
var game = ServerDataModel.addOne(params);
// get the id of the new resource to populate the Location field
var gameid = game.gameid;
return [201, game, { Location: '/games/' + gameid }];
});
// this is the update of an existing resource (ngResource does not send PUT for update)
$httpBackend.whenPOST(/\/games\/\d+/).respond(function(method, url, data) {
var params = angular.fromJson(data);
// parse the matching URL to pull out the id (/games/:id)
var gameid = url.split('/')[2];
var game = ServerDataModel.updateOne(gameid, params);
return [201, game, { Location: '/games/' + gameid }];
});
// this is the update of an existing resource (ngResource does not send PUT for update)
$httpBackend.whenDELETE(/\/games\/\d+/).respond(function(method, url, data) {
// parse the matching URL to pull out the id (/games/:id)
var gameid = url.split('/')[2];
ServerDataModel.deleteOne(gameid);
return [204, {}, {}];
});
$httpBackend.whenGET(/templates\//).passThrough();
});
angular.module('app').service('ServerDataModel', function ServerDataModel() {
this.data = [
{
gameid: 1,
opponent: "Tech",
date: new Date(2014, 4, 7),
attendance: 2038
},
{
gameid: 2,
opponent: "State",
date: new Date(2014, 4, 13),
attendance: 1655
},
{
gameid: 3,
opponent: "College",
date: new Date(2014, 4, 20),
attendance: 1897
}
];
this.getData = function() {
return this.data;
};
this.setData = function(data) {
this.data = data;
};
this.findOne = function(gameid) {
// find the game that matches that id
var list = $.grep(this.getData(), function(element, index) {
return (element.gameid == gameid);
});
if(list.length === 0) {
return {};
}
// even if list contains multiple items, just return first one
return list[0];
};
this.findAll = function() {
return this.getData();
};
// options parameter is an object with key value pairs
// in this simple implementation, value is limited to a single value (no arrays)
this.findMany = function(options) {
// find games that match all of the options
var list = $.grep(this.getData(), function(element, index) {
var matchAll = true;
$.each(options, function(optionKey, optionValue) {
if(element[optionKey] != optionValue) {
matchAll = false;
return false;
}
});
return matchAll;
});
};
// add a new data item that does not exist already
// must compute a new unique id and backfill in
this.addOne = function(dataItem) {
// must calculate a unique ID to add the new data
var newId = this.newId();
dataItem.gameid = newId;
this.data.push(dataItem);
return dataItem;
};
// return an id to insert a new data item at
this.newId = function() {
// find all current ids
var currentIds = $.map(this.getData(), function(dataItem) { return dataItem.gameid; });
// since id is numeric, and we will treat like an autoincrement field, find max
var maxId = Math.max.apply(Math, currentIds);
// increment by one
return maxId + 1;
};
this.updateOne = function(gameid, dataItem) {
// find the game that matches that id
var games = this.getData();
var match = null;
for (var i=0; i < games.length; i++) {
if(games[i].gameid == gameid) {
match = games[i];
break;
}
}
if(!angular.isObject(match)) {
return {};
}
angular.extend(match, dataItem);
return match;
};
this.deleteOne = function(gameid) {
// find the game that matches that id
var games = this.getData();
var match = false;
for (var i=0; i < games.length; i++) {
if(games[i].gameid == gameid) {
match = true;
games.splice(i, 1);
break;
}
}
return match;
};
});
<h2>Use save(game) to update a single game</h2>
<form name="updateGetForm" role="form">
<div class="form-group">
<label for="updateGameid">Enter Gameid to update</label>
<input type="number" class="form-control" ng-required="true" ng-model="updateGameid" name="updateGameid" id="updateGameid" />
</div>
<div class="form-group">
<button type="button" class="btn btn-primary" ng-click="updateGetResource(updateGameid)">Get single game using $resource - ResourceService.get(gameid)</button>
<button type="button" class="btn btn-danger" ng-click="updateReset()">Reset</button>
</div>
</form>
<form name="updateForm" role="form" ng-hide="updateGame === null">
<div class="form-group">
<label for="updateOpponent">Opponent:</label>
<input type="text" class="form-control" ng-model="updateGame.opponent" name="updateOpponent" id="updateOpponent"/>
</div>
<div class="form-group">
<label for="updateDate">Date:</label>
<p class="input-group">
<input type="date" class="form-control" datepicker-popup="dd-MMMM-yyyy" is-open="opened.update" datepicker-options="dateOptions" ng-required="true" close-text="Close" ng-model="updateGame.date" name="updateDate" id="updateDate" />
<span class="input-group-btn">
<button type="button" class="btn btn-default" ng-click="open($event, 'update')"><i class="glyphicon glyphicon-calendar"></i></button>
</span>
</p>
</div>
<div class="form-group">
<label for="updateAttendance">Attendance</label>
<input type="number" class="form-control" ng-model="updateGame.attendance" name="updateAttendance" id="updateAttendance" />
</div>
<div class="form-group">
<button type="button" class="btn btn-primary" ng-click="updateHttp(updateGame)">Update game using $http - HttpService.save()</button>
<button type="button" class="btn btn-primary" ng-click="updateResource(updateGame)">Update game using $resource - ResourceService.save()</button>
<button type="button" class="btn btn-danger" ng-click="updateReset()">Reset</button>
</div>
</form>
<h4>query() Results after save(). Validate that the item you updated shows up correctly in query() output and server data shown below</h4>
<div class="row">
<div class="col-md-6">
<pre>{{ updateResults | json}}</pre>
</div>
</div>
<h2>Use get(gameid) to retrieve a single record</h2>
<form name="getForm" role="form">
<div class="form-group">
<label for="getGameid">Enter Gameid to get()</label>
<input type="number" class="form-control" ng-required="true" ng-model="getGameid" name="getGameid" id="getGameid"/>
</div>
<div class="form-group">
<button type="button" class="btn btn-primary" ng-click="getHttp(getGameid)">Get game using $http - HttpService.get(gameid)</button>
<button type="button" class="btn btn-primary" ng-click="getResource(getGameid)">Get game using $resource - ResourceService.get(gameid)</button>
<button type="button" class="btn btn-danger" ng-click="getReset()">Reset</button>
</div>
</form>
<h4>get(gameid) Results (compare to server data below, should match the appropriate gameid record)</h4>
<div class="row">
<div class="col-md-6">
<pre>{{ getResults | json}}</pre>
</div>
</div>
<form name="queryForm" role="form">
<div class="form-group">
<button type="button" class="btn btn-primary" ng-click="queryHttp()">Query using $http - HttpService.query()</button>
<button type="button" class="btn btn-primary" ng-click="queryResource()">Query using $resource - ResourceService.query()</button>
<button type="button" class="btn btn-danger" ng-click="queryReset()">Reset</button>
</div>
</form>
<h4>query() Results (compare to server data below, should match exactly as a query() returns all records)</h4>
<div class="row">
<div class="col-md-6">
<pre>{{ queryResults | json}}</pre>
</div>
</div>
<form name="gameForm" role="form">
<div class="form-group">
<label for="createFormOpponent">Opponent:</label>
<input type="text" class="form-control" ng-model="game.opponent" name="opponent" id="createFormOpponent"/>
</div>
<div class="form-group">
<label for="createDate">Date:</label>
<div class="input-group">
<input type="text" class="form-control" datepicker-popup="dd-MMMM-yyyy" ng-model="game.date" is-open="opened.create" datepicker-options="dateOptions" ng-required="true" close-text="Close" name="createDate" id="createDate" />
<span class="input-group-btn">
<button type="button" class="btn btn-default" ng-click="open($event, 'create')"><i class="glyphicon glyphicon-calendar"></i></button>
</span>
</div>
</div>
<div class="form-group">
<label for="createAttendance">Attendance:</label>
<input type="number" class="form-control" ng-model="game.attendance" name="attendance" id="createAttendance" />
</div>
<div class="form-group">
<button type="button" class="btn btn-primary" ng-click="saveHttp(game)">Add using $http - HttpService</button>
<button type="button" class="btn btn-primary" ng-click="saveResource(game)">Add using $resource - ResourceService</button>
<button type="button" class="btn btn-danger" ng-click="reset()">Reset</button>
</div>
</form>
<h2>Use delete() to delete a single game</h2>
<form name="deleteForm" role="form">
<div class="form-group">
<label for="deleteGameid">Enter Gameid to delete</label>
<input type="number" class="form-control" ng-required="true" ng-model="deleteGameid" name="deleteGameid" id="deleteGameid"/>
</div>
<div class="form-group">
<button type="button" class="btn btn-primary" ng-click="deleteHttp(deleteGameid)">Delete game using $http - HttpService.delete()</button>
<button type="button" class="btn btn-primary" ng-click="deleteResource(deleteGameid)">Delete game using $resource - ResourceService.delete()</button>
<button type="button" class="btn btn-danger" ng-click="deleteReset()">Reset</button>
</div>
</form>
<h4>query() Results after delete(). Validate that the item you deleted is not present in query() output and server data shown below</h4>
<div class="row">
<div class="col-md-6">
<pre>{{ deleteResults | json}}</pre>
</div>
</div>