AngularJS – CRUD Data Grid I

Published by

on

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!

30 responses to “AngularJS – CRUD Data Grid I”

  1. Jannen Siahaan Avatar

    Hi,
    I’m trying this but stop on “BaseValidatableViewModel”, where it come from?

    Like

    1. Juan Carlos Sánchez Avatar

      Hi Jannen,

      BaseValidatableViewModel is a base class that map between Model – ViewModel. You can implement your own class using Automapper. This class also validates the ViewModel using the Model, but this is out of the scope of this post.

      if you are interested in the implementation I can share it with you.

      Thanks for your comment!

      Like

      1. jsiahaan Avatar

        Thanks Carlos,

        Could you please share the implementation with me!

        Regards

        Like

    2. Juan Carlos Sánchez Avatar

      Hi Jannen, you can find more details in my new post:

      AutoMapper – Model/ViewModel mapping and validation

      Like

  2. AngularJS – CRUD Data Grid II | Juan Carlos Sanchez's Blog Avatar

    […] AngularJS – CRUD Grid I […]

    Like

  3. AngularJS – CRUD Data Grid III | Juan Carlos Sanchez's Blog Avatar

    […] AngularJS – CRUD Data Grid I […]

    Like

  4. Zeph Grunschlag Avatar
    Zeph Grunschlag

    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

    1. Juan Carlos Sánchez Avatar

      Hi Zeph,

      Thank you for your comment!

      I also considered the ng-grid but I wanted to implement my own control (directive) to learn more about angular-js. I could also add additional functionality like, date picker or edit in place.

      The ng-grid is now being re-implemented:
      http://ui-grid.info/

      Best regards,
      Juan

      Like

  5. Palchuhai Avatar
    Palchuhai

    i don’t know how to work it? help please! 😀

    Like

  6. Abdurehman Yousaf Avatar

    HI Jaun ! that’s a great post…can you ghelp me to create a CRUD app with angular with PHP Slim for serverside and routing with ui-router/

    Like

  7. Juan Carlos Sánchez Avatar

    Hi Abdurehman, the grid should work if your server side offers the required API. Would be great if you can offer an example of the grid working with PHP after you make it work… Good luck!

    Like

  8. Balram Avatar
    Balram

    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

    1. Juan Carlos Sánchez Avatar

      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

  9. Balram Avatar
    Balram

    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

  10. Balram Avatar
    Balram

    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

  11. Juan Carlos Sánchez Avatar

    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

  12. Balram Avatar
    Balram

    Works like a charm.
    Thanks man!!!

    Liked by 1 person

  13. Balram Avatar
    Balram

    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

  14. Juan Carlos Sánchez Avatar

    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

  15. Balram Avatar
    Balram

    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

  16. Tim Avatar
    Tim

    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

    1. Juan Carlos Sánchez Avatar

      Thank you! I am glad to know that you found it useful.

      Like

  17. coolarchtek Avatar
    coolarchtek

    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

    1. Juan Carlos Sánchez Avatar

      Hi! Thank you for your comment.

      I recommend you to read this, maybe it helps you:
      http://www.ng-newsletter.com/posts/directives.html

      Like

  18. Kashif Avatar
    Kashif

    How would you implement paging in this gridView?

    Like

  19. Juan Carlos Sánchez Avatar

    I would write a service on the server side that allows pagination and adapt the ui to send the page or the start – end item that you want to display. I hope it helps! 🙂

    Like

  20. NAZIRAHN Avatar
    NAZIRAHN

    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

    1. Juan Carlos Sánchez Avatar

      Hi Nazirhan, thank you for your comment.
      I dont have a video tutorial sorry. Regarding the refresh problem I don’t get it, do you have the same problem if you refresh the data without filters?

      Like

  21. anu Avatar
    anu

    From now i will follow your blog.. gives best solution !! 🙂 🙂 keep going on …

    Wow awesome.. impressive UI. but it is taking time to go through. Yet i am using your grid to implement in my project. without using webapi i need to put some dummy data like hard coded values. can you please help me on this where can i insert in angular js only.

    Like

Your feedback is important…

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Blog at WordPress.com.