AngularJS Tutorial - 6

New version here

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

<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

Screenshot

Download the sample source


Last modified on: September 15, 2014