AngularJS – CRUD Data Grid I

The goal of this post is to write a single page application (SPA) to display a list of items inside a grid. This grid shall not only display a list of items but also allows all the CRUD operations: Create a new item and Read, Update and Delete for the contained items.

Additional requirements:

  • Show a notification when a server request success or fail.
  • Editing an item: Start editing with a double click and commit the edition with “Enter”.
  • Everything shall be executed without refreshing the web page
  • The complete list of items should only be requested one when the page is loaded.

UPDATE:

Results

Code is available on github: https://github.com/softwarejc/angularjs-crudgrid. Feel free to play with it, find errors and write improvements. You can see a live demo here: AngularJS CRUD Grid III demo.

This is a screenshot of the AngularJS Crud Grid that we are going to implement:

AngularJS Crud Grid

Implementation

Let’s get our hands dirty, we will cover the following points:

  1. Domain: Entity Framework + Domain Services
  2. Server side: WebAPI + ViewModels
  3. Client side: AngularJS + Bootstrap + Toastr
  4. What’s next?

The architecture of the application is the same I proposed on my previous post:

Single Page Application (SPA) using AngularJS and WebAPI – Architecture

Domain

At the lower level of the application I use Entity Framework – Code first. Further details here are out of the scope of this post.  I used the simplest domain class I could imagine to focus on the goal of this post. This class is call “Item” and just contains a string property “Name”. The only restriction is that this “Name” should be unique.

public class Item
{
    [Key]
    public int Id { getset; }
 
    [Index(IsUnique = true)]
    public string Name { getset; }
}

We could use directly the DbContext inside our WebAPI controllers but I think it is better to add an additional layer here with the domain logic. This layer should contain the logic to manipulate items. This is the box “Domain Services” of my Architecture Diagram.

This is the interface of this the service:

public interface IBaseService<T>
{
    T Add(T item);
    Item Get(int id);
    IEnumerable<T> GetAll();
    bool Update(T updatedItem);
    bool Remove(int id);
}
 
public interface IItemsService : IBaseService<Item>
{
    Item Get(string name);
    bool Remove(string name);
}

Server Side

At the server side I use a REST Service using Microsoft WebAPI:

public class KnownItemsController : ApiController
   {
       readonly IItemsService _itemsService = new ItemsService();
 
       // Create item
       public HttpResponseMessage Post(ItemViewModel item)
       {
           if (!ModelState.IsValid || item == null || string.IsNullOrEmpty(item.Name))
           {
               throw new HttpResponseException(HttpStatusCode.ExpectationFailed);
           }
 
           var addedItem = _itemsService.Add(item.MapToEntity());
           return Request.CreateResponse(HttpStatusCode.Created, addedItem);
       }
 
       // Read all items
       public IEnumerable<ItemViewModel> Get()
       {
           return _itemsService.GetAll().Select(ItemViewModel.MapFromEntity);
       }
 
       // Read item by id
       public ItemViewModel Get(int id)
       {
           var item = _itemsService.Get(id);
           if (item == null)
           {
               throw new HttpResponseException(HttpStatusCode.NotFound);
           }
           return ItemViewModel.MapFromEntity(item);
       }
 
       // Update item
       public void Put([FromBody]ItemViewModel updatedItem)
       {
           if (!_itemsService.Update(updatedItem.MapToEntity()))
           {
               throw new HttpResponseException(HttpStatusCode.NotFound);
           }
       }
 
       // Remove item
       public bool Delete(int id)
       {
           var item = _itemsService.Get(id);
           if (item == null)
           {
               throw new HttpResponseException(HttpStatusCode.NotFound);
           }
 
           return _itemsService.Remove(id);
       }
   }

I use a view model that is mapped from and to the domain class. This view model could be used to send calculated fields to the client or to hide some information of our domain.

public class ItemViewModel : BaseValidatableViewModel<ItemViewModelItem>
   {
       public int Id { getset; }
       public string Name { getset; }
   }

I use a base class to map between view model and domain. This class can also be used to validate the view model using the model.

Client Side

The CRUD Grid is already inside a html angular template that will be injected where the ui-view is defined.

<div class="row">
 
    <!--AngularJS SPA CRUD table-->
    <table class="table table-condensed table-hover table-striped" ng-hide="loading">
        <!-- Header-->
        <tr class="panel-title">
            <!--Buttons column-->
            <th style="width100px;">
                <!--Add + Cancel-->
                <div class="btn-toolbar"><i class="btn glyphicon glyphicon-plus" ng-click="toggleAddMode()" title="Add" ng-hide="addMode"></i></div>
                <div class="btn-toolbar"><i class="btn glyphicon glyphicon-minus" ng-click="toggleAddMode()" title="Cancel" ng-show="addMode"></i></div>
            </th>
 
            <!-- Content columns-->
            <th style="vertical-align:middle">Name</th>
        </tr>
 
        <!--Row with the new item-->
        <tr ng-show="addMode" style="backgroundrgb(251, 244, 222)">
            <!--Buttons column-->
            <td>
                <div class="btn-toolbar">
                    <!--Create + Cancel-->
                    <div class="btn-group">
                        <i class="btn glyphicon glyphicon-floppy-disk" ng-click="createItem()" title="Create" ng-disabled="addForm.$invalid"></i>
                        <i class="btn glyphicon glyphicon-remove" ng-click="toggleAddMode()" title="Cancel"></i>
                    </div>
                </div>
            </td>
            <!-- Content columns-->
            <td>
                <form name="addForm">
                    <input type="text" ng-model="newItem.name" class="form-control" required placeholder="new item..." ng-keydown="saveOnEnter(knownItem, $event)" required />
                </form>
            </td>
        </tr>
 
        <!--Content-->
        <tr ng-repeat="knownItem in knownItems">
            <!--Buttons column-->
            <td>
                <!--Edit + Delete-->
                <div class="btn-toolbar" ng-show="knownItem.editMode == null || knownItem.editMode == false">
                    <div class="btn-group">
                        <i class="btn glyphicon glyphicon-edit" ng-click="toggleEditMode(knownItem)" title="Edit"></i>
                        <i class="btn glyphicon glyphicon-trash" ng-click="deleteItem(knownItem)" title="Delete" data-toggle="modal"></i>
                    </div>
                </div>
 
                <!--Save + Cancel-->
                <div class="btn-toolbar" ng-show="knownItem.editMode">
                    <div class="btn-group">
                        <i class="btn glyphicon glyphicon-floppy-disk" ng-click="updateItem(knownItem)" title="Save" ng-disabled="editForm.$invalid"></i>
                        <i class="btn glyphicon glyphicon-remove" ng-click="toggleEditMode(knownItem)" title="Cancel"></i>
                    </div>
                </div>
            </td>
 
            <!-- Content columns-->
            <td style="vertical-align:middle">
                <!--Name read mode-->
                <span ng-show="knownItem.editMode == null || knownItem.editMode == false" ng-dblclick="toggleEditMode(knownItem)">
                    {{knownItem.name}}
                </span>
                <!--Name edit mode-->
                <form name="editForm">
                    <input ng-model="knownItem.name" ng-show="knownItem.editMode" required ng-keydown="updateOnEnter(knownItem, $event)" class="form-control" />
                </form>
            </td>
        </tr>
    </table>
 
    <!--Loading indicator-->
    <img src="../images/loading.gif" ng-show="loading" class="center-block" title="Loading..." />
 
</div>

This is my index file:

<!DOCTYPE html>
<html ng-app="stockModule"
      xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>Angular JS - CRUD Grid (I)</title>
        <!--css-->
        <link href="Content/bootstrap.css" rel="stylesheet" />
        <link href="Content/toastr.css" rel="stylesheet" />
 
        <!--jquery-->
        <script src="Scripts/jquery-2.1.1.js"></script>
 
        <!--toaster-->
        <script src="Scripts/toastr.js"></script>
 
        <!--angular-->
        <script src="Scripts/angular.js"></script>
        <script src="Scripts/angular-resource.js"></script>
        <script src="Scripts/AngularUI/ui-router.js"></script>
 
 
        <!--my scripts-->
        <script src="Angular/StockModule.js"></script>
        <script src="Angular/NotificationFactory.js"></script>
        <script src="Angular/KnownItems/KnownItemsController.js"></script>
        <script src="Angular/KnownItems/KnownItemsFactory.js"></script>
    </head>
 
    <!--Angular APP container (ng-app)-->
    <body class="container" ng-cloak>
        <div class="navbar">
            <div>
                <a href="#">Angular JS Crud Grid</a>
                <p>This is a running demo of the AngularJS CRUD Grid I describe in my blog:</p>
                <a href="http://www.softwarejuancarlos.com">softwarejuancarlos.com</a>
            </div>
        </div>
 
        <!--The grid will be injected here-->
        <div ui-view>
        </div>
    </body>
 
 
</html>

Last but not least, the AngularJS code. The angular files:

  • The angular module (AKA app) definition.
  • The notifications factory to encapsulate the usage of toastr.
  • The items factory to encapsulate all REST calls to the server using ng-resource.
  • The items controller

Angular module/app definition:

var stockModule = angular.module('stockModule', ['ui.router','ngResource']);
 
stockModule.config(function ($stateProvider, $urlRouterProvider) {
 
    // For any unmatched URL, redirect to stock
    $urlRouterProvider.otherwise("/knownItems");
 
    $stateProvider
        // Known items
        .state('knownItems', {
            url: "/knownItems",
            templateUrl: "partials/knownItems.html",
            controller: "knownItemsController"
        });
 
});

Notification factory (toastr):

stockModule.factory('notificationFactory'function () {
    toastr.options = {
        "showDuration""100",
        "hideDuration""100",
        "timeOut""2000",
        "extendedTimeOut""5000",
    }
 
    return {
        success: function (text) {
            if (text === undefined) {
                text = '';
            }
            toastr.success("Success. " + text);
        },
        error: function (text) {
            if (text === undefined) {
                text = '';
            }
            toastr.error("Error. " + text);
        },
    };
});

Items factory (ng-Resource):

// Repository 
stockModule.factory("knownItemsFactory"function ($resource) {
    return $resource('/api/KnownItems/:id',
    {
        // default URL params
        // @ Indicates that the value should be obtained from a data property
        id: '@Id'
    },
    {
        // add update to actions (is not defined by default)
        'update': { method: 'PUT' }
    });
});

Angular controller, this is the most interesting file, I tried to add a lot of comments here:

stockModule.controller("knownItemsController"function ($scope, knownItemsFactory, notificationFactory) {
 
    // PRIVATE FUNCTIONS 
    var requestSuccess = function () {
        notificationFactory.success();
    }
 
    var requestError = function () {
        notificationFactory.error();
    }
 
    var isNameDuplicated = function (itemName) {
        return $scope.knownItems.some(function (entry) {
            return entry.name.toUpperCase() == itemName.toUpperCase();
        });
    };
 
    var isDirty = function(item) {
        return item.name != item.serverName;
    }
 
    // PUBLIC PROPERTIES
 
    // all the items
    $scope.knownItems = [];
    // the item being added
    $scope.newItem = { name: '' };
    // indicates if the view is being loaded
    $scope.loading = false;
    // indicates if the view is in add mode
    $scope.addMode = false;
 
    // PUBLIC FUNCTIONS
 
    // Toggle the grid between add and normal mode
    $scope.toggleAddMode = function () {
        $scope.addMode = !$scope.addMode;
 
        // Default new item name is empty
        $scope.newItem.name = '';
    };
 
    // Toggle an item between normal and edit mode
    $scope.toggleEditMode = function (item) {
        // Toggle
        item.editMode = !item.editMode;
 
        // if item is not in edit mode anymore
        if (!item.editMode) {
            // Restore name
            item.name = item.serverName;
        } else {
            // save server name to restore it if the user cancel edition
            item.serverName = item.name;
 
            // Set edit mode = false and restore the name for the rest of items in edit mode 
            // (there should be only one)
            $scope.knownItems.forEach(function (i) {
                // item is not the item being edited now and it is in edit mode
                if (item.id != i.id && i.editMode) {
                    // Restore name
                    i.name = i.serverName;
                    i.editMode = false;
                }
            });
        }
    };
 
    // Creates the 'newItem' on the server
    $scope.createItem = function () {
        // Check if the item is already on the list
        var duplicated = isNameDuplicated($scope.newItem.name);
 
        if (!duplicated) {
            knownItemsFactory.save($scope.newItem,
            // success response
            function (createdItem) {
                // Add at the first position
                $scope.knownItems.unshift(createdItem);
                $scope.toggleAddMode();
 
                requestSuccess();
            },
            requestError);
        } else {
            notificationFactory.error("The item already exists.");
        }
    }
 
    // Gets an item from the server using the id
    $scope.readItem = function (itemId) {
        knownItemsFactory.get({ id: itemId }, requestSuccess, requestError);
    }
 
    // Updates an item
    $scope.updateItem = function (item) {
        item.editMode = false;
 
        // Only update if there are changes
        if (isDirty(item)) {
            knownItemsFactory.update({ id: item.id }, item, function (success) {
                requestSuccess();
            }, requestError);
        }
    }
 
    // Deletes an item
    $scope.deleteItem = function (item) {
        knownItemsFactory.delete({ id: item.id }, item, function (success) {
            requestSuccess();
            // Remove from scope
            var index = $scope.knownItems.indexOf(item);
            $scope.knownItems.splice(index, 1);
        }, requestError);
    }
 
    // Get all items from the server
    $scope.getAllItems = function () {
        $scope.loading = true;
        $scope.knownItems = knownItemsFactory.query(function (success) {
            $scope.loading = false;
        }, requestError);
    };
 
    // In edit mode, if user press ENTER, update item
    $scope.updateOnEnter = function (item, args) {
        // if key is enter
        if (args.keyCode == 13) {
            $scope.updateItem(item);
            // remove focus
            args.target.blur();
        }
    };
 
    // In add mode, if user press ENTER, add item
    $scope.saveOnEnter = function (item, args) {
        // if key is enter
        if (args.keyCode == 13) {
            $scope.createItem();
            // remove focus
            args.target.blur();
        }
    };
 
 
    // LOADS ALL ITEMS
    $scope.getAllItems();
});

What’s next?:

  • Add a confirmation dialog before deleting an item.
  • Add the AngularJS Crud grid to a directive.
  • Dynamic columns.
  • Allow columns ordering.

Please leave a comment!

29 thoughts on “AngularJS – CRUD Data Grid I

  1. Zeph Grunschlag says:

    Hi Juan,

    This is a super helpful post! Thank you for presenting such a complete example.

    I’m wondering if you considered using ng-grid instead of creating your own grid functionality?

    If you did consider this, what were some factors that led you to implement it on your own?

    -Zeph

    Liked by 1 person

  2. Balram says:

    Hi Juan,
    Very nice post and code example.

    Could you please let me know how can I get row selection functionality? I want to implement Master/Detail scenario where once I click any row in grid, I will show selected item’s information.

    Thanks in advance.

    Best Regards,
    Balram

    Liked by 1 person

    • Hi Balram,

      Thank you for your comment.

      You could implement it adding a ‘+’ icon that is the trigger to show the details row.
      If the icon is not a valid solution for you it would be necessary to modify the grid directive to add that feature. If you implement it I would be glad to merge your changes 🙂

      Best regards,
      Juan

      Like

  3. Balram says:

    Hi Jaun,

    As my requirement changed, it was no longer required to have row selection. I have shown a div in current row on which + button is clicked which was easy part as you are already passing current row as parameter.
    Very generic and well organized code you have given.

    Thank you again!

    Best Regards,
    Balram

    Like

  4. Balram says:

    Hi Jaun,
    Need your help please.
    I am trying make grid data source url as configurable from controller. But it is not recognizing.

    Below is grid change:

    <div crud-grid
    column-button-click='itemsCtrl.gridOnButtonClick'
    initialized='itemsCtrl.gridOnInitialized'
    server-url='itemsCtrl.getAllServiceUrl'
    columns-definition='[

    Below is item.controller.js section for url:

    self.getAllServiceUrl = _getAllServiceUrl;

    function _getAllServiceUrl() {
    var serviceUrl = "http://localhost:8095/testData/&quot; + ajaxServiceFactory.getCustomerId() + "/info/";
    return serviceUrl;
    }

    When I run it, I get below error:
    Failed to load resource: the server responded with a status of 404 (Not Found) http://localhost:56652/itemsCtrl.getAllServiceUrl

    Seem it is not resolving the getAllServiceUrl from controller.

    Could you please let me know how to set url from controller?

    Thanks!

    Best Regards,
    Balram

    Like

  5. Hi Balram,

    I just checked in a small update to allow bindings to the server-url.

    Now you can do something like:

    <div crud-grid
    column-button-click='itemsCtrl.gridOnButtonClick'
    initialized='itemsCtrl.gridOnInitialized'
    server-url='{{itemsCtrl.serverUrl}}'

    Let me know if this solution solves your problem.

    Thank you for your feedback.

    Best regards,
    Juan

    Like

  6. Balram says:

    Hi Jaun,

    As I am going forward with your grid, I am coming up with few challenges and interesting points.

    I am not sure if you have this feature already there or not.

    Actually currently your grid load data only once with serverurl set. If I want to change data source on ngclick method handler, its not working.
    Initially grid will be empty. We will have input box which will accept server data url and one button which load data into grid.

    Could you please let me know if it’s possible and how to do it?

    Thanks in advance.

    Best Regards,
    Balram

    Like

  7. Hi Balram,

    There is an event triggered ‘gridOnInitialized’ that pass as parameter the grid controller. You can attach to this event adding this to your directive definition:
    ‘ initialized=’itemsCtrl.gridOnInitialized’

    One you have the grid controller you can try to trigger again a ‘initialization’. I am not sure 100% sure if this would work but I would try it.

    If this doesn’t work maybe you need to add a refresh method to the controller passing the new URL as a parameter 🙂

    If you implement it please send me a pull request 🙂

    Good luck and best regards,

    PS: Are you working in an open source project? I would like to see how you use the grid 🙂

    Like

  8. Balram says:

    Hi Jaun,

    I am using your grid in my company project for one proof of concept. If that gets approved I will merge it our website.
    If I understand correctly, there is no licence or legal issues of using your grid, right?
    Kindly let me know if there is any use case obligation for using your grid in commercial project.

    Thanks!

    Best regards,
    Balram

    Like

  9. Tim says:

    I’ve seen a LOT of tutorials and this is the first and only one that is a complete example of a proper CRUD in angular with REST backend, which in turn also keeps state on front-end. Most importantly, it is really well documented and made me understand the basics of Angular quite well. A lot of thanks and props to you sir :).

    Liked by 1 person

  10. coolarchtek says:

    Juan, Nice informative website/blog. Thanks! I have a question for you, I am planning to design user interface for business in my company. We have list of tables (lets say 100) and I need to design
    CRUD screen for each for table. Instead of creating 100 screens is there way to do this using usercontrol in AngularJS? I just input table name to control and it generates UI.

    Like

  11. NAZIRAHN says:

    Hi Juan, well explain post. I just want to ask, I planning on making design interface. I hard code my data to appear in table. I did not use any DB to pull the data. I have create data using array. I successfully display and create some filter operation so user can easily find the data. I also have create add operation and it can successfully add into table but once i have refresh it, it gone. Is there any way for me to make new data stay in the table? My second question is did you have video tutorial for above code example?

    Like

Your feedback is important...

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