AngularJS + ASP.NET Web API: Building a simple grid in AngularJS with server-side paging, sorting, searching (Part 8)

This post will be the culmination of all of the following posts:

In part 1 I described how to configure the application to include the requisite JavaScript files, laid out the folder structure & stubbed out the app.js and a few other JavaScript files.

In Part 2 I populated the grid with data returned from the Web API.

Part 3 of the post incorporated the ability to page through the grid.

In part 4, I added a loading bar / progress bar

In part 5, we took a brief detour and I spoke about unit testing

Part 6 – we saw how to sort records

And, Part 7 – added the ability to search based on first and last names.

In this post – part 8, I’ll be adding the ability to edit a record in-place as well as delete a record, and also add a new record.

Let’s start by writing the functions that will handle the deleting a record and updating an existing record or adding a new record.

Let’s first write the data access layer on the client side, the dataService should now look as follows


    .factory("dataService", ["$http", "$q", function ($http, $q) {

        var _students = [];

        var _getStudents = function (options) {

            var deferred = $q.defer();

            $http.get("api/StudentsApi?currentPage=" + options.currentPage + "&" +
                "recordsPerPage=" + options.recordsPerPage + "&" +
                "sortKey=" + options.sortKeyOrder.key + "&" + "sortOrder=" + options.sortKeyOrder.order + "&searchfor=" + options.searchfor)
                .then(

                function (result) {
                    angular.copy(result.data.students, _students);
                    deferred.resolve(result.data.recordCount);
                },

                function () {
                    deferred.reject();
                });

            return deferred.promise;
        };

        var _postStudent = function (record) {

            var deferred = $q.defer();

            $http.post("api/StudentsApi", record).then(

                function (result) {
                    deferred.resolve(result.data);
                },

                function () {
                    deferred.reject();
                }
            );

            return deferred.promise;
        };

        var _deleteStudent = function (id) {

            var deferred = $q.defer();

            $http.delete("api/StudentsApi/" + id).then(

                function (result) {
                    deferred.resolve(result.data);
                },

                function () {
                    deferred.reject();
                }

            );

            return deferred.promise;

        }

        return {
            students: _students,
            getStudents: _getStudents,
            postStudent: _postStudent,
            deleteStudent: _deleteStudent,
        };
    }])

Notice that for the delete request, the id is being passed and the postStudent function will handle both the updates as well as adding a new record. Whether a new record should be added or an existing record updated will be handled by the web api and we’ll get to that shortly.

Now that we’ve written the the dataService functions that interact with the backend api, the next step will be write the controller functions that interact with these dataService functions.

But before we jump into writing the controller functions let’s create a service that will allow us to inject a modal confirmation dialog box as shown in the figure below. Essentially, this modal confirmation dialog asks the user if they would like to carry out a given interaction or inform the user if some event has occured that users need to aware of.

Untitled

The modal confirmation dialog will have 3 parts

  1. The first part is the template or html code that defines the look & feel of the popup modal
  2. The second part is the controller
  3. The third part is the modal service itself

Let’s first design the template for this modal dialog, create a file named modalConfirmation.tpl.html & place it in the app/simpleGrid folder.

The code inside this tpl.html file would look similar to this

<div class="modal-header">
  <h3 class="modal-title">{{title}}</h3>
</div>
<div class="modal-body">
  <label>{{message}}</label>
</div>
<div class="modal-footer">
    <button class="btn btn-primary" ng-click="ok()">OK</button>
  <button class="btn btn-warning" ng-click="cancel()" ng-show="showCancel">Cancel</button>
</div>

The modal confirmation controller would look as so


    .controller('modalConfirmationInstanceCtrl', ["$scope", "$modalInstance", "message",
        "title", "id", "showCancel", function ($scope, $modalInstance, message, title, id, showCancel) {

            $scope.message = message;
            $scope.title = title;
            $scope.showCancel = showCancel;

            $scope.ok = function () {
                $modalInstance.close();
            };

            $scope.cancel = function () {
                $modalInstance.dismiss('cancel');
            };

        }])

Finally, the modal confirmation service itself will look at so,


 .factory("modalConfirmationService", ["$log", "$modal", function ($log, $modal) {

            var _getModalInstance = function (title, message, id, showCancel) {

                return $modal.open({
                    templateUrl: 'app/ratingentity/modalConfirmation.tpl.html',
                    controller: 'modalConfirmationInstanceCtrl',
                    size: 'sm',
                    resolve: {
                        title: function () {
                            return title;
                        },
                        message: function () {
                            return message;
                        },
                        id: function () {
                            return id;
                        },
                        showCancel: function () {
                            return showCancel;
                        },
                    }
                });

            };

            return {
                getModalInstance: _getModalInstance
            };
        }])

As you can see in the highlighted lines above i.e. lines 6,7 that the modal confirmation service references the template/html code and also the modal confirmation controller we just wrote.

Next step is to write controller functions to delete and save a record, so now the controller should include the following additional functions. Remember that the save functions will be used to both create a new record as well as update an existing record. As mentioned earlier, whether a record has to be created or updated will be handled by the backend API.

Before we write these two additional functions let’s inject the recently created modal confirmation service into the student controller, so the controller’s signature should look as so


.controller("studentCtrl", ["$scope", "dataService", "localStorageService", "modalConfirmationService",
        function ($scope, dataService, localStorageService, modalConfirmationService) {

And, the two new functions that have been added to the studentCtrl are shown below


                 $scope.save = function (id) {

                var record = $scope.data.filter(function (v) { return v["id"] == id; });

                dataService.postStudent(record[0])
                    .then(function (updatedStudentRecord) {
                        modalConfirmationService.getModalInstance("the following record has been updated", "updated student record with last name = " + updatedStudentRecord.lastName);
                    },
                        function () {
                            modalConfirmationService.getModalInstance("failed to save the changes, please try again");
                        });
            };


            $scope.delete = function (id) {

                var record = $scope.data.filter(function (v) { return v["id"] == id; });

                modalConfirmationService.getModalInstance("delete record", "are you sure you would like to delete the student record with last name = " + record[0].lastName, id, true).result.
                    then(function () {
                        dataService.deleteStudent(id)
                            .then(function (deletedRecord) {
                                modalConfirmationService.getModalInstance("deleted record", "record with last name = " + deletedRecord.lastName + " has been deleted!");
                                getData($scope, $http, dataService, localStorageService, modalConfirmationService);
                            }, function () {
                                modalConfirmationService.getModalInstance("error occured", " an error occured while attempting to delete record, please try again");
                            });

                    }, function () {
                        //called when modal confirmation dialog is dismissed
                    });

            };


In line 19 the modal confirmation service’s getModalInstance function is called and here the modal options that you would like to use, header and the main text and whether the cancel button should be shown are supplied. You’ll notice that in line 20 when the user selects either the ok or cancel button then the $modal.open function’s return object returns a promise. The first parameter of this promise is a callback function in line 20 and this function gets called when the user selects ok in the modal dialog box. And, the second parameter of the promise is also a callback function as shown in line 29 and this function gets called when the user selects cancel in the modal dialog box.

So, if the user selects ok i.e. the user is saying “yes – go ahead and delete the selected student” then line 21 will be executed and this dataService’s deleteStudent function will also return a promise, and again the first parameter is a callback function which gets executed if the delete was successful and second parameter is a callback function which gets called if the delete was not successful. In both these scenarios we will popup a modal confirmation to let the user know what happened.

In the data service’s post student function the situation is a bit simpler, here depending on whether the student was successfully updated or not, a modal confirmation is shown to the user.

Next, lets write the UI pieces i.e. the save and delete buttons, these will be added to the students.tpl.html. These buttons i.e. the save & delete buttons will trigger the postStudent function and deleteStudent functions respectively in the studentCtrl. But rather than directly adding these buttons to the template, we’ll take the route of creating custom element directives and then adding these elements to the students.tpl.html file.

The code for the custom element directives will look as so


     .directive("saveButton", [function () {
         return {
             restrict: "E",
             replace: true,
             scope: {
                 text: "@",
                 action: "&",
                 comment: "="
             },
             template: "<button type='button' class='btn btn-primary' style='width: 75px;height: 30px' ng-click='action()'>{{text}}</button>"
         };
     }])

    .directive("deleteButton", [function () {
        return {
            restrict: "E",
            replace: true,
            scope: {
                text: "@",
                cssclass:"@",
                action: "&",
                comment: "="
            },
            template: "<button type='button' class='{{cssclass}}' style='width: 75px;height: 30px' ng-click='action()'>{{text}}</button>"
        };
    }])

Take a look at this link, this should help you get started with AngularJS directives. Directives are a very convenient way of packaging reusable functionality. So, in our case by creating save and delete element directives what we have done is that now we can use these elements on multiple templates.

Next, lets place these directives on the students.tpl.html page as so, see lines 44 & 47 below


<div class="row">
    <div class="col-xs-4 col-sm-3 col-md-3">
        <div class="input-group">
            <input type="search" class="form-control" placeholder="Search for..." ng-model="searchfor">
            <div class="input-group-btn">
                <button class="btn btn-default" type="button" ng-click="search(searchfor)"><i class="glyphicon glyphicon-search"></i></button>
            </div>
        </div>
    </div>
</div>

<div class="row top-buffer">
    <table class="table table-bordered table-striped table-responsive">
        <thead>
            <tr>
                <th>
                </th>
                <th>
                </th>
                <th>
                </th>

                <th>
                    <a href="#" ng-click="sort('lastName')" target="_self">Last Name</a>
                    <i ng-class="{'glyphicon glyphicon-chevron-up':sortKeyOrder.order=='ASC' && sortKeyOrder.key=='lastName'}"></i>
                    <i ng-class="{'glyphicon glyphicon-chevron-down':sortKeyOrder.order=='DESC' && sortKeyOrder.key=='lastName'}"></i>

                </th>
                <th>
                    <a href="#" ng-click="sort('firstName')" target="_self">First Name</a>
                    <i ng-class="{'glyphicon glyphicon-chevron-up':sortKeyOrder.order=='ASC' && sortKeyOrder.key=='firstName'}"></i>
                    <i ng-class="{'glyphicon glyphicon-chevron-down':sortKeyOrder.order=='DESC' && sortKeyOrder.key=='firstName'}"></i>
                </th>
                <th>
                    Date of Enrollment
                </th>

            </tr>
        </thead>
        <tbody data-ng-repeat="i in data">
            <tr>
                <td></td>
                <td>
                    <save-button text="Save" action="save(i.id)"></save-button>
                </td>
                <td>
                    <delete-button cssclass="btn btn-danger button-top-buffer" text="Delete" action="delete(i.id)"></delete-button>
                </td>
                <td>
                    <textarea class="form-control" style="width: 300px;height: 65px" ng-model="i.lastName"></textarea>
                </td>
                <td>
                    <textarea class="form-control" style="width: 300px;height: 65px" ng-model="i.firstMidName"></textarea>
                </td>
                <td>
                    <input type="text" class="form-control" style="width: 150px;height: 65px" ng-model="i.enrollmentDate" />
                </td>
            </tr>

        </tbody>
    </table>

    <span data-pagination data-total-items="totalItems" data-ng-model="currentPage" data-max-size="numberOfPageButtons" class=" pagination-sm" data-boundary-links="true" data-rotate="false" data-ng-change="pageChanged()" data-items-per-page="recordsPerPage"></span>


</div>

If you recall in Part 2 of this series of blog posts we had generated the CRUDs for the backend WebAPI, so now if you run the application you should see a save and delete button next to each record in the grid and you should be able to make changes to any record in the grid and save it or even delete that record.

You’ll also notice that after each save and delete interaction, a helpful modal confirmation dialog should be displayed telling you what happened, and if error occured then that too should cause a modal dialog to open up.

Finally, all that is now left to do is to add the ability to create a new record.

First, let’s define a new route, when the user attempts to create a new student record she will be taken to a fresh page, see lines 8 to 11 below


angular.module('ngWebApiGrid.student', ['ngRoute', 'ui.bootstrap', 'chieffancypants.loadingBar'])
    .config(["$routeProvider", function ($routeProvider) {
        $routeProvider.when("/", {
            controller: "studentCtrl",
            templateUrl: "app/simpleGrid/students.tpl.html"
        });

        $routeProvider.when("/new-student", {
            controller: "newStudentCtrl",
            templateUrl: "app/simpleGrid/newStudent.tpl.html"
        });

        $routeProvider.otherwise("/");
    }])


Notice in line 10 above, we are referring to a template by the name of newStudent.tpl.html, let’s go ahead and create that.

Add a new html page to the simpleGrid folder and name it newStudent.tpl.html.

The code within the newStudent.tpl.html should like so


<h4>Create a new student profile</h4>

<div class="row">
    <form name="newStudentForm" novalidate data-ng-submit="save()">
        <div class="form-horizontal">

            <div class="form-group">
                <div class="control-label col-md-2">Last Name</div>
                <div class="col-md-10">
                    <textarea class="form-control" cols="20" name="lastName" rows="2" data-ng-model="newStudent.lastName" required data-ng-maxlength="50" maxlength="50"></textarea>
                    <span class="field-validation-error" data-ng-show="newStudentForm.lastName.$error.required">Required!</span>
                </div>
            </div>

            <div class="form-group">
                <div class="control-label col-md-2">First Name</div>
                <div class="col-md-10">
                    <textarea class="form-control" cols="20" ng-model="newStudent.firstMidName" name="firstMidName" rows="2" maxlength="50"></textarea>
               </div>
            </div>

            <div class="form-group">
                <div class="control-label col-md-2">Enrollment Date</div>
                <div class="col-md-10">
                    <textarea class="form-control" cols="20" ng-model="newStudent.enrollmentDate" name="enrollmentDate" rows="2" required maxlength="50"></textarea>
                    <span class="field-validation-error" data-ng-show="newStudentForm.enrollmentDate.$error.required">Required!</span>
                 </div>
            </div>

            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <input type="submit" value="Create" class="btn btn-primary" data-ng-disabled="newStudentForm.$invalid" />
                </div>
            </div>
        </div>

    </form>
</div>

<div class="row">
    <div>
        <a class="btn btn-primary" href="#/">back to list</a>
    </div>
</div>

The code above should be self explanatory, essentially there are 3 fields that the user can input, and of these three fields the last name field is required. A word of caution here, for the enrollment date, though I have made this a required field, I’m not ensuring whether the date field is in the correct format. So, if an invalid date is entered then an error will be returned and the modal dialog will pop up informing the user that the record was not created.

To learn more about how AngularJS form validations work, take a look quick look at this.

Next, lets add the code for the newStudentCtrl to the student.js file as so


.controller("newStudentCtrl", ["$scope", "$http", "$window", "dataService", "modalConfirmationService",
        function ($scope, $http, $window, dataService, modalConfirmationService) {

            $scope.newStudent = {

            };

            $scope.save = function () {

                dataService.postStudent($scope.newStudent)
                    .then(
                        function (newStudent) {
                            modalConfirmationService.getModalInstance("record saved", "added student with last name =" + newStudent.lastName);

                        },
                        function () {
                            modalConfirmationService.getModalInstance("record saved", "could not save the new student, please try again");

                        })
                    .then(function () {
                        $window.location = "#";
                    });
            };
        }])

All that’s now left to do is add a way to get to the new-student template and we do that by adding the following lines of code to the students.tpl.html as so


<div class="row">
    <div class="col-xs-4 col-sm-3 col-md-3">
        <a type="button" class="btn btn-primary" href="#/new-student">Create a new student</a>
    </div>
</div>

As always the entire source lies at https://github.com/SangeetAgarwal/NgWebApiGrid, and the changes I made in this post can be seen at the following SHA.

Please feel free to fork the repository and if you see any bugs feel free to submit a pull request.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s