New version Angular 9
Now we should delete, update and add items. We understood the importance of testing and we will start with a TDD like approach.
We will first add the tests for the ContactsRepositoryService. We define a standard message content that will be passed for POST/PUT requests.
var messageContent = { id:"objId", content:"Message Content"}; it("save should make an ajax post call to api/contacts/", function () { $httpBackend.whenPOST("api/contacts/",messageContent).respond([{ result: "ok", data :{id:"objId"} }]); expect(contactsRepositoryService.save(messageContent)).toBeDefined(); });
Then running the specRunner.html should give us an error. We should add to CcontactsRepositoryService the save function, that will simply do a POST to the api passing the contact as a parameter.
app.factory('contactsRepositoryService',function($http) { return { ... save : function(contact) { return this.http.post(this.apiBase, contact); } } });
And now the test will pass!
We should now add the tests for delete and update. Note that we are adding the "request_method" parameter, since not all servers allow the usage of the DELETE and PUT verbs. So we will simulate it through a POST for the PUT, and with GET for a DELETE.
it("update should make an ajax post call to api/contacts/?id=objId&request_method=PUT", function () { $httpBackend.whenPOST("api/contacts/?id=objId&request_method=PUT",messageContent).respond([{ result: "ok", data :{id:"objId"} }]); expect(contactsRepositoryService.update(messageContent)).toBeDefined(); }); it("deleteById should make an ajax get call to api/contacts/?id=objId&request_method=DELETE", function () { $httpBackend.whenGET("api/contacts/?id=objId&request_method=DELETE").respond([{ result: "ok", data :{id:"objId"} }]); expect(contactsRepositoryService.deleteById('objId')).toBeDefined(); });
This will not pass until we don't add the methods to the repository
update : function(contact) { return this.http.post(this.apiBase+'?id='+contact.id+'&request_method=PUT', contact); }, deleteById : function(contactId) { return this.http.get(this.apiBase+'?id='+contactId+'&request_method=DELETE'); }
We should now add the mock functions to our mock repository. So that we can test everything
function buildMockRepository(dataToReturn = {},returnsFunction = null){ ... save : function(item) { this.calledMethod = "save"; this.calledObject = item; return returnsFunction(); }, update : function(item) { this.calledMethod = "update"; this.calledObject = item; return returnsFunction(); }, deleteById : function(id) { this.calledMethod = "deleteById"; this.calledObject = id; return returnsFunction(); } ...
First we will create categories for the functions inside the detailController spec. And we setup a mock object to be used for our test, even inside the repositoryMock.
var mockObject = { id:id }; var repositoryService = buildMockRepository(mockObject); describe("open", function() { //All open related functions }); describe("add", function() { //All open related functions });
Then we could setup the add method test, and we expect everything to fail.
describe("add", function() { it("add function should be added to scope", function() { expect(scope.add).toBeDefined(); }); it('add function should call the $modal open',function(){ scope.add(); expect(mockModal.openCalled).toBe(true); }); it('add function should call the $modal with the correct parameters',function(){ scope.add(); expect(mockModal.openParam).not.toBe(null); expect(mockModal.openParam.templateUrl).toBe("assets/test/add.html"); expect(mockModal.openParam.controller).toBe("detailControllerModalAdd"); }); });
We should then write the add function inside the detailController. Note that we added too a reset function. This is needed to reset to empty the value of the item inside the scope. $scope.master will be a kind of constant that will allow to reset the content of the variable scope.
$scope.master = {}; $scope.activePath = null; $scope.reset = function() { $scope[identifier] = angular.copy($scope.master); }; $scope.add = function () { var modalInstance = $modal.open({ templateUrl: 'assets/'+identifier+'/add.html', controller: 'detailControllerModalAdd', resolve: { repository: function () {return repository;} } }); modalInstance.result.then(function () { $scope.reset(); $scope.activePath = $location.path('/'); }); };
And its controller, that will add the save function on the scope and upon success closes the dialgo and reload the current route, aka reloading the index.html with a fresh list. Even a cancel function is added to close without further action. Note that we pass the $route service that will handle all connections with the routing engine.
app.controller('detailControllerModalAdd', ['$scope', '$route', '$modalInstance', 'repository', function($scope, $route, $modalInstance, repository) { $scope.save = function (item) { repository.save(item) .success(function(){ $modalInstance.close(); $route.reload(); })}; $scope.cancel = function () { $modalInstance.close(); }; } ]);
Now we should create a new template, "assets/contacts/add.html" for the add dialog
<div class="modal-header"> <h2>Add new Contact</h2> </div> <div class="modal-body"> <form novalidate name="addNewForm" id="add-new-form" method="post" action=""> <label for="name">First Name:</label> <input type="text" ng-model="contact.name" required /> <label for="surname">Last Name:</label> <input type="text" ng-model="contact.surname" required /> <label for="address">Address:</label> <input type="text" ng-model="contact.address" required /> <label for="phone">Phone:</label> <input type="text" ng-model="contact.phone" /> </form> </div> <div class="modal-footer"> <button class="btn btn-primary" ng-disabled="addNewForm.$invalid || isUnchanged(contact)" id="add-new-btn" ng-click="save(contact)">Save!</button> <button class="btn" ng-click="cancel()">Cancel</button> </div>
And connect the button on the list.html file
<button class="btn btn-primary" ng-click="add()" >Add New Contact</button>
Now the Update and Delete actions will be a lot easier to understand
Now you can reuse heavily the DetailsController and the ListController as a template for the various CRD (not yet update) operations you need to perform