AngularJS Tutorial - 6
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.
The Services
Add testing
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();
});
Add implementation
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!
Update and Delete testing
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();
});
Update and Delete implementation
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');
}
Updating the repository mock
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();
}
...
The detailController, the add function
Add function test
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");
});
});
Add function implementation
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('/');
});
};
Add function implementation, detailControllerModalAdd
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
- ng-model: bidirectional binding with the item we are working on. These values will be used to create a contact object inside the $scope
- ng-disabled: when the content of the 'addNewForm' is invalid the button save will be disabled (we will use this later adding filters) or when the model associated to the form is unchanged. If you want to save an empty form the Save button will not work!
- ng-click="save(contact)": will save the model passing it to the controller. Note that this "save" method is the one inside the ModalAddController. You can't use the "add" method inside the DetailController since the scope of the dialog is a child of the DetailController scope
<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>
Update and Delete
Now the Update and Delete actions will be a lot easier to understand
Notes
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
