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:
Implementation
Let’s get our hands dirty, we will cover the following points:
- Domain: Entity Framework + Domain Services
- Server side: WebAPI + ViewModels
- Client side: AngularJS + Bootstrap + Toastr
- What’s next?
The architecture of the application is the same I proposed on my previous post:
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 { get; set; } [Index(IsUnique = true)] public string Name { get; set; } }
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<ItemViewModel, Item> { public int Id { get; set; } public string Name { get; set; } }
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="width: 100px;"> <!--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="background: rgb(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!
Hi,
I’m trying this but stop on “BaseValidatableViewModel”, where it come from?
LikeLike
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!
LikeLike
Thanks Carlos,
Could you please share the implementation with me!
Regards
LikeLike
Hi Jannen, you can find more details in my new post:
https://softwarejuancarlos.com/2014/09/12/automapper-modelviewmodel-mapping-and-validation/
LikeLike
Pingback: AngularJS – CRUD Data Grid II | Juan Carlos Sanchez's Blog
Pingback: AngularJS – CRUD Data Grid III | Juan Carlos Sanchez's Blog
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
LikeLiked by 1 person
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
LikeLike
i don’t know how to work it? help please! 😀
LikeLike
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/
LikeLike
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!
LikeLike
Pingback: AJAX CRUD operations - Collins Creative
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
LikeLiked 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
LikeLike
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
LikeLike
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/" + 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
LikeLike
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
LikeLike
Works like a charm.
Thanks man!!!
LikeLiked by 1 person
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
LikeLike
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 🙂
LikeLike
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
LikeLike
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 :).
LikeLiked by 1 person
Thank you! I am glad to know that you found it useful.
LikeLike
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.
LikeLike
Hi! Thank you for your comment.
I recommend you to read this, maybe it helps you:
http://www.ng-newsletter.com/posts/directives.html
LikeLike
How would you implement paging in this gridView?
LikeLike
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! 🙂
LikeLike
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?
LikeLike
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?
LikeLike
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.
LikeLike