The goal of this post is to extend the AngularJS CRUD Grid that we created in a previous post: AngularJS – CRUD Grid I 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.
In that post we wrote a single page application (SPA) to display a list of items inside a grid. The grid allows all the CRUD operations: Create, Read, Update and Delete items. We will add now the following requirements:
- # 1 Confirmation dialog before deleting an item.
- # 2 Column ordering.
- # 3 CRUD Grid as a directive
- # 3.1 Dynamic columns generation.
- # 3.2 Column options: Type, visibility, header, mandatory.
- # 4 Cell editor directive, text and date mode. Use AngularJS date picker in date format columns
Results
You can see a demo here: AngularJS CRUD Grid II demo. I will share the interesting code of the application in this post and one of my next steps will be to upload all the code to GitHub.
Running application
Delete confirmation
Date picker
Implementation
- # 1 Confirmation dialog before deleting an item.
- # 2 Column ordering.
- # 3 CRUD Grid as a directive
- # 4 Cell editor directive, text and date mode. Use AngularJS date picker in date format columns
Server side
To do all the implementation we will use the same infrastructure we have used for the previous post at the server side. We will only add a couple of additional properties to test the dynamic column generation:
- Description: String not required
- Expire: DateTime not required
public class Item { [Key] public int Id { get; set; } [MaxLength(200)] [Index(IsUnique = true)] public string Name { get; set; } public string Description { get; set; } [DataType(DataType.Date)] public DateTime? Expire { get; set; } }
# 1 Confirmation dialog before deleting an item.
I use $modal from UI Bootstrap AngularJS. I created a Modal Window Factory with a ‘show’ method. This method has four parameters:
- title: Window’s title.
- msg: A message to show in the window’s body.
- confirmCallback: A callback to execute when the user click “OK”.
- cancelCallback: A callback to execute when the user click “Cancel”.
The crud grid controller use this factory when the user want to delete an item:
function _deleteItem(item) { var title = "Delete '" + item.name + "'"; var msg = "Are you sure you want to remove this item?"; var confirmCallback = function () {...}; modalWindowFactory.show(title, msg, confirmCallback); };
This is the factory:
stockModule.factory('modalWindowFactory', function ($modal) { var modalWindowController = _modalWindowController; return { // Show a modal window with the specified title and msg show: function (title, msg, confirmCallback, cancelCallback) { // Show window var modalInstance = $modal.open({ templateUrl: 'partials/modalWindow.html', controller: modalWindowController, size: 'sm', resolve: { title: function () { return title; }, body: function () { return msg; } } }); // Register confirm and cancel callbacks modalInstance.result.then( // if any, execute confirm callback function() { if (confirmCallback != undefined) { confirmCallback(); } }, // if any, execute cancel callback function () { if (cancelCallback != undefined) { cancelCallback(); } }); } }; // Internal controller used by the modal window function _modalWindowController($scope, $modalInstance, title, body) { $scope.title = ""; $scope.body = ""; // If specified, fill window title and message with parameters if (title) { $scope.title = title; } if (body) { $scope.body = body; } $scope.confirm = function () { $modalInstance.close(); }; $scope.cancel = function () { $modalInstance.dismiss(); }; }; });
The UI:
<div> <!--Header--> <div class="modal-header"> <h3 class="modal-title">{{title}}</h3> </div> <!--Body--> <div class="modal-body"> <p>{{body}}</p> </div> <!--OK and Cancel buttons--> <div class="modal-footer"> <button class="btn btn-success" ng-click="confirm()" style="width: 100px;"> <i class="glyphicon glyphicon-ok"></i> OK </button> <button class="btn btn-danger" ng-click="cancel()" style="width: 100px;"> <i class="glyphicon glyphicon-remove"></i> Cancel </button> </div> </div>
# 2 Columns ordering
This portion of code does the ordering selection:
<!-- Content headers--> <th ng-repeat="column in itemsCtrl.columnsDefinition" ng-hide="column.hidden" style="vertical-align:middle; cursor: pointer"> <div ng-click="itemsCtrl.setOrderByColumn(column.binding)"> {{column.header}} <i class="glyphicon" ng-class="{'glyphicon glyphicon-arrow-up': !itemsCtrl.orderByReverse, 'glyphicon glyphicon-arrow-down': itemsCtrl.orderByReverse}" ng-show="itemsCtrl.orderByColumn == column.binding"></i> </div> </th>
When we click the header, it calls the controller with the column that was clicked has parameter to set it as active ordering column. The controller also checks if the column being ordered is the same, in this case it reverses the ordering direction:
function _setOrderByColumn(column) { if (self.orderByColumn == column) { // change order self.orderByReverse = !self.orderByReverse; } else { // order using new column self.orderByColumn = column; self.orderByReverse = false; } }
To finish this requirement we add the ordering configuration to the ng-repeat that print a line for each item:
<tr ng-repeat="knownItem in itemsCtrl.allItems | orderBy:itemsCtrl.orderByColumn: itemsCtrl.orderByReverse">
# 3 CRUD Grid as a Directive
To use our data grid in different places we need it as a directive. This directive will also take care of dynamic columns generation. In our example we want to generate a column not only for the item name but also for the description and expire property. This is how I would like to use the grid:
- I add the ‘crud-grid’ directive to the item where I want the grid.
- I define the address where the REST service should access my WebAPI.
- I add a column definition for each item property I want to see on my grid.
Each column definition can have this options:
- Binding: The property of the received JSON that this column will display
- Header: The header of this column
- Type: The type of this column, currently text or date.
- Required: If true this column cannot be empty before saving changes.
- Hidden: If true this column is hidden. Used for example to add the id during implementation.
Sample of usage:
<div class="row"> <div crud-grid server-url='/api/KnownItems' columns-definition='[ { "binding" :"id", "type" :"text", "required" :"true", "hidden" :"true" }, { "binding" :"name", "header" :"Name", "type" :"text", "required" :"true" }, { "binding" :"description", "header" :"Description", "type" :"text", "required" :"false" }, { "binding" :"expire", "header" :"Expire Date", "type" :"date", "required" :"false" } ]'> </div> </div>
We will divide the implementation in 4 files:
- Directive definition
- Controller
- Items Factory
- View
Directive definition
stockModule.directive('crudGrid', function () { return { // 'A' - only matches attribute name // 'E' - only matches element name // 'C' - only matches class name restrict: 'A', // Don't replace the element that contains the attribute replace: false, // scope = false, parent scope // scope = true, get new scope // scope = {..}, isolated scope scope: true, // view templateUrl: '/app/directives/crud.grid/crud.grid.view.html', // controller controller: "crudgridController as itemsCtrl" } });
Controller
stockModule.controller("crudgridController", itemsController); function itemsController($element, $attrs, ajaxServiceFactory, notificationsFactory, modalWindowFactory) { 'use strict'; var self = this; //// ---------------- PUBLIC ----------------- //// PUBLIC fields // Columns he grid should display self.columnsDefinition = []; // All items self.allItems = []; // The item being added self.newItem = {}; // Indicates if the view is being loaded self.loading = false; // Indicates if the view is in add mode self.addMode = false; // The column used for ordering self.orderByColumn = ''; // Indicates if the ordering is reversed or not self.orderByReverse = false; //// PUBLIC Methods // Initialize module self.initialize = _initialize; // Toggle the grid between add and normal mode self.toggleAddMode = _toggleAddMode; // Toggle an item between normal and edit mode self.toggleEditMode = _toggleEditMode; // Creates the 'newItem' on the server self.createItem = _createItem; // Gets an item from the server using the id self.readItem = _readItem; // Updates an item self.updateItem = _updateItem; // Deletes an item self.deleteItem = _deleteItem; // Get all items from the server self.getAllItems = _getAllItems; // In edit mode, if user press ENTER, update item self.updateModeKeyUp = _updateModeKeyUp; // In add mode, if user press ENTER, add item self.createModeKeyUp = _createModeKeyUp; // Set the order by column and order self.setOrderByColumn = _setOrderByColumn; //// ---------------- CODE TO RUN ------------ self.initialize(); //// ---------------- PRIVATE ---------------- //// PRIVATE fields var _itemsService; //// PRIVATE Functions - Public Methods Implementation function _initialize() { // create a service to do the communication with the server _itemsService = ajaxServiceFactory.getService($attrs.serverUrl); // configured columns self.columnsDefinition = angular.fromJson($attrs.columnsDefinition); // Initialize self.getAllItems(); } function _toggleAddMode() { self.addMode = !self.addMode; // Empty new item self.newItem = {}; // Set an default id or the validation will crash self.newItem.id = 0; self.newItem.hasErrors = !_isValid(self.newItem); }; function _toggleEditMode(item) { // Toggle item.editMode = !item.editMode; // if item is not in edit mode anymore if (!item.editMode) { // Undo changes _restoreServerValues(item); } else { // save server name to restore it if the user cancel edition item.serverValues = angular.toJson(item); // Set edit mode = false and restore the name for the rest of items in edit mode // (there should be only one) self.allItems.forEach(function (i) { // item is not the item being edited now and it is in edit mode if (item.id != i.id && i.editMode) { // Save current editing values self.updateItem(i); } }); } }; function _createItem(item) { if (_isValid(item)) { _itemsService.save(item, // success response function (createdItem) { // Add at the first position self.allItems.unshift(createdItem); self.toggleAddMode(); _requestSuccess(response); }, // error callback function (error) { _requestError(error); }); } }; function _readItem(itemId) { _itemsService.get({ id: itemId }, _requestSuccess, _requestError); }; function _updateItem(item) { if (_isValid(item)) { item.editMode = false; // Only update if there are changes if (_isDirty(item)) { _itemsService.update({ id: item.id }, item, function (response) { // Refresh item with server values _copyItem(response, item); _requestSuccess(); }, function (error) { _requestError(error); _restoreServerValues(item); }); } } }; function _deleteItem(item) { var title = "Delete '" + item.name + "'"; var msg = "Are you sure you want to remove this item?"; var confirmCallback = function () { return _itemsService.delete( // id { id: item.id }, // item item, // success callback function () { _requestSuccess(); // Remove from scope var index = self.allItems.indexOf(item); self.allItems.splice(index, 1); }, // error callback function (error) { _requestError(error); }); }; modalWindowFactory.show(title, msg, confirmCallback); }; function _getAllItems() { self.loading = true; self.allItems = _itemsService.query(function () { // success callback self.loading = false; }, // error callback function () { _requestError("Error loading items."); }); }; function _updateModeKeyUp(args, item) { // if key is enter if (args.keyCode == 13) { // update self.updateItem(item); // remove focus args.target.blur(); } // refresh validation item.hasErrors = !_isValid(item); }; function _createModeKeyUp(args, item) { // if key is enter if (args.keyCode == 13) { // create self.createItem(item); // remove focus args.target.blur(); } // refresh validation item.hasErrors = !_isValid(item); }; function _setOrderByColumn(column) { if (self.orderByColumn == column) { // change order self.orderByReverse = !self.orderByReverse; } else { // order using new column self.orderByColumn = column; self.orderByReverse = false; } } //// PRIVATE Functions function _requestSuccess() { notificationsFactory.success(); }; function _requestError(error) { notificationsFactory.error(error.statusText); }; function _isValid(item) { var isValid = true; // validate all columns self.columnsDefinition.forEach(function (column) { if (isValid) { // required validation if (column.required == 'true') { isValid = item[column.binding] != undefined; } } }); return isValid; }; function _isDirty(item) { var serverItem = angular.fromJson(item.serverValues); var isDirty = false; self.columnsDefinition.forEach(function (column) { if (!isDirty && // short circuit if item is dirty (item[column.binding] != serverItem[column.binding])) { isDirty = true; } }); return isDirty; }; function _restoreServerValues(item) { var serverItem = angular.fromJson(item.serverValues); _copyItem(serverItem, item); self.columnsDefinition.forEach(function (column) { item[column.binding] = serverItem[column.binding]; }); } function _copyItem(itemSource, itemTarget) { self.columnsDefinition.forEach(function (column) { itemTarget[column.binding] = itemSource[column.binding]; }); } };
Items Factory
// Repository stockModule.service("ajaxServiceFactory", ajaxServiceFactory); function ajaxServiceFactory($resource) { 'use strict'; //// PUBLIC METHODS - Definition this.getService = _getService; //// PUBLIC METHODS - Implementation function _getService(endPoint) { if (endPoint === '') { throw "Invalid end point"; } // create resource to make AJAX calls return $resource(endPoint + '/:id', { id: '@Id' // default URL params, '@' Indicates that the value should be obtained from a data property }, { 'update': { method: 'PUT' } // add update to actions (is not defined by default) }); } }
View
<div class="row"> <!--AngularJS SPA CRUD table--> <form name="tableForm"> <table class="table table-condensed table-hover table-striped" ng-hide="itemsCtrl.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="itemsCtrl.toggleAddMode()" title="Add" ng-hide="itemsCtrl.addMode"></i></div> <div class="btn-toolbar"><i class="btn glyphicon glyphicon-minus" ng-click="itemsCtrl.toggleAddMode()" title="Cancel" ng-show="itemsCtrl.addMode"></i></div> </th> <!-- Content headers--> <th ng-repeat="column in itemsCtrl.columnsDefinition" ng-hide="column.hidden" style="vertical-align:middle; cursor: pointer"> <div ng-click="itemsCtrl.setOrderByColumn(column.binding)"> {{column.header}} <i class="glyphicon" ng-class="{'glyphicon glyphicon-arrow-up': !itemsCtrl.orderByReverse, 'glyphicon glyphicon-arrow-down': itemsCtrl.orderByReverse}" ng-show="itemsCtrl.orderByColumn == column.binding"></i> </div> </th> </tr> <!--Row with the new item--> <tr ng-if="itemsCtrl.addMode" style="background: rgb(251, 244, 222)"> <!--New item: Buttons column--> <td> <div class="btn-toolbar"> <!--Create + Cancel--> <div class="btn-group"> <i class="btn glyphicon glyphicon-floppy-disk" ng-click="itemsCtrl.createItem(itemsCtrl.newItem)" title="Create" ng-disabled="itemsCtrl.newItem.hasErrors"></i> <i class="btn glyphicon glyphicon-remove" ng-click="itemsCtrl.toggleAddMode()" title="Cancel"></i> </div> </div> </td> <!-- New item: Content columns--> <td ng-repeat="column in itemsCtrl.columnsDefinition" ng-hide="column.hidden" style="vertical-align:middle"> <!--Show cell editor control for each column--> <div cell-editor item="itemsCtrl.newItem" column="column" key-up-event="itemsCtrl.createModeKeyUp"> </div> </td> </tr> <!--Content--> <tr ng-repeat="knownItem in itemsCtrl.allItems | orderBy:itemsCtrl.orderByColumn: itemsCtrl.orderByReverse"> <!--Buttons column--> <td class="col-xs-1"> <!--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="itemsCtrl.toggleEditMode(knownItem)" title="Edit"></i> <i class="btn glyphicon glyphicon-trash" ng-click="itemsCtrl.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="itemsCtrl.updateItem(knownItem)" title="Save" ng-disabled="knownItem.hasErrors"></i> <i class="btn glyphicon glyphicon-remove" ng-click="itemsCtrl.toggleEditMode(knownItem)" title="Cancel"></i> </div> </div> </td> <!-- Content columns--> <td ng-repeat="column in itemsCtrl.columnsDefinition" ng-hide="column.hidden" style="vertical-align:middle" class="col-xs-2"> <!--Read mode--> <span ng-show="knownItem.editMode == null || knownItem.editMode == false" ng-dblclick="itemsCtrl.toggleEditMode(knownItem)"> <!-- Each item as an array property / value, header is the value that this column display--> <span ng-switch="column.type"> <!-- Text --> <span ng-switch-default>{{knownItem[column.binding]}}</span> <!-- Date --> <span ng-switch-when="date">{{knownItem[column.binding]| date:'fullDate'}}</span> </span> </span> <!--Show cell editor control for each column--> <div ng-show="knownItem.editMode" cell-editor item="knownItem" column="column" key-up-event="itemsCtrl.updateModeKeyUp"> <!--updateModeKeyUp has no parenthesis to pass parameters--> </div> </td> </tr> </table> </form> <!--Loading indicator--> <img src="/images/loading.gif" ng-show="itemsCtrl.loading" class="center-block" title="Loading..." /> </div>
#4 Cell editor directive, text and date mode.
To display an editor for each cell depending on the column type I created a dedicated directive. This directive uses the AngularJS UI Datepicker when the column type is date. This directive has 3 files:
- Directive definition
- Controller
- View
Directive definition
stockModule.directive('cellEditor', function () { return { // 'A' - only matches attribute name // 'E' - only matches element name // 'C' - only matches class name restrict: 'A', // Replace the element that contains the attribute replace: true, // scope = false, parent scope // scope = true, get new scope // scope = {...}, isolated scope> // 1. "@" ( Text binding / one-way ) // 2. "=" ( Model binding / two-way ) // 3. "&" ( Method binding ) scope: { column: "=", // object binding item: "=", // object binding keyUpEvent: "&", // method binding }, // view templateUrl: '/app/directives/crud.grid/cell.editor/cell.editor.view.html', // controller controller: "cellEditorController as cellEditorCtrl" } });
Controller
stockModule.controller("cellEditorController", cellEditorController); function cellEditorController($scope) { 'use strict'; var self = this; //// ---------------- PUBLIC ----------------- //// PUBLIC fields self.keyUpEvent = $scope.keyUpEvent; self.column = $scope.column; self.item = $scope.item; self.datePickerOpen = false; self.openDatePicker = _openDatePicker; self.fireKeyUpEvent = _fireKeyUpEvent; //// PUBLIC Methods //// ---------------- CODE TO RUN ------------ //// ---------------- PRIVATE ---------------- //// PRIVATE fields //// PRIVATE Functions - Public Methods Implementation function _fireKeyUpEvent(args, item) { // call method with parameters self.keyUpEvent()(args, item); }; function _openDatePicker($event) { $event.preventDefault(); $event.stopPropagation(); self.datePickerOpen = true; }; //// PRIVATE Functions };
View
<div ng-switch="cellEditorCtrl.column.type" > <!--Text required--> <input ng-switch-default type="text" required ng-if="cellEditorCtrl.column.required != 'false'" ng-model="cellEditorCtrl.item[cellEditorCtrl.column.binding]" ng-keyup="cellEditorCtrl.fireKeyUpEvent($event, cellEditorCtrl.item)" class="form-control" /> <!--Text not required--> <input ng-switch-default type="text" ng-if="cellEditorCtrl.column.required != 'true'" ng-model="cellEditorCtrl.item[cellEditorCtrl.column.binding]" ng-keyup="cellEditorCtrl.fireKeyUpEvent($event, cellEditorCtrl.item)" class="form-control" /> <!--Date required--> <span class="input-group" ng-switch-when="date" ng-if="cellEditorCtrl.column.required == 'false'"> <input type="text" class="form-control" datepicker-popup="fullDate" ng-model="cellEditorCtrl.item[cellEditorCtrl.column.binding]" is-open="cellEditorCtrl.datePickerOpen" ng-keyup="cellEditorCtrl.fireKeyUpEvent($event, cellEditorCtrl.item)" /> <span class="input-group-btn"> <button type="button" class="btn btn-default" ng-click="cellEditorCtrl.openDatePicker($event)"> <i class="glyphicon glyphicon-calendar"></i> </button> </span> </span> <!--Date not required--> <span class="input-group" ng-switch-when="date" ng-if="cellEditorCtrl.column.required == 'true'"> <input type="text" class="form-control" datepicker-popup="fullDate" ng-model="cellEditorCtrl.item[cellEditorCtrl.column.binding]" is-open="cellEditorCtrl.datePickerOpen" ng-keyup="cellEditorCtrl.fireKeyUpEvent($event, cellEditorCtrl.item)" /> <span class="input-group-btn"> <button type="button" class="btn btn-default" ng-click="cellEditorCtrl.openDatePicker($event)"> <i class="glyphicon glyphicon-calendar"></i> </button> </span> </span> </div>
What’s next?:
- Date columns extended configuration: min and max date, date format.
- Unit tests.
- Upload code to GitHub.
- Implement some of you ideas! Leave a comment.
Pingback: AngularJS – CRUD Grid I | Juan Carlos Sanchez's Blog
Pingback: AngularJS – CRUD Data Grid III | Juan Carlos Sanchez's Blog
Hi, your link to demo links to different one. Could you please have a look at it and update? I’d really appreciate it.
LikeLike
Hi, the link to the demo is: http://crud-grid-angular-iii.azurewebsites.net
It includes the changes of my third post about the angular Crud Grid.
LikeLike
Hi, I am actually looking for the one with date picker. If you have that example and could share, I’d appreciate it.
LikeLike
Hi Sndpmalla,
The link I sent you has now a date column.
Regards
LikeLike
Hi, can you please put the whole code in plunker or git hub .
It will be highly appriciated.
Thanks in advanced.
LikeLike
Hi, this is the link to the code in github:
https://github.com/softwarejc/angularjs-crudgrid
LikeLike
Hi, I have saw your demo and it was wonderful! But sadly i did not manage to catch it. Could you give me a clearer instruction on how to implement the functions step by step?
LikeLike
Hi, Thank you for your email. Tell me what is your problem exactly and I will try to help you. 🙂
LikeLike
do you have same demo done in java.
LikeLike
nop, sorry
LikeLike
How hard would it be to add category headers, to group rows?
LikeLike
I am not sure if I understood your question.
Do you need something like this (WPF example)? http://www.c-sharpcorner.com/uploadfile/dpatra/grouping-in-datagrid-in-wpf/
If yes, it can be implemented but not easily, row grouping is not supported at the moment.
LikeLike