Expandable grid in AngularJS-UI-Grid with Web API

If you want to know how to get started with UI-Grid and how to set up a project in AngularJS and Web API, read the articles given below first.

Angular-UI-Grid has expandable feature which adds the sub grid feature to grid. You can load any data source when click on expand icon.

In this article, I am going to use NORTHWND database and Employee, Employee Territories tables. My feature grid will load all employees and when click on expand then employees territories will load in sub grid.

After creating all basic and adding mandatory script, let’s implement expandable feature on ui-grid. Thus, after adding data in Entity Model, create a Web API to get the data from Entity model.

Entity Model


Web API

Here, my Web API class code is given below.

  1. using ng_ui_grid_sample.Models;  
  2. using System.Collections.Generic;  
  3. using System.Data.Entity;  
  4. using System.Data.Entity.Infrastructure;  
  5. using System.Linq;  
  6. using System.Net;  
  7. using System.Threading.Tasks;  
  8. using System.Web.Http;  
  9. using System.Web.Http.Description;  
  10. namespace ng_ui_grid_sample.Controllers  
  11. {  
  12. [RoutePrefix("api/employeeapi")]  
  13. public class EmployeesAPIController : ApiController  
  14. {  
  15. private NORTHWNDEntities3 db = new NORTHWNDEntities3();  
  16. // GET: api/Employees  
  17. [Route("get")]  
  18. public IQueryable<Employee> GetEmployees()  
  19. {  
  20. return db.Employees;  
  21. }  
  22. // GET: api/Employees/5  
  23. [Route("detail")]  
  24. [ResponseType(typeof(Employee))]  
  25. public async Task<IHttpActionResult> GetEmployees(int id)  
  26. {  
  27. Employee employees = await db.Employees.FindAsync(id);  
  28. if (employees == null)  
  29. {  
  30. return NotFound();  
  31. }  
  32. return Ok(employees);  
  33. }  
  34. // PUT: api/Employees/5  
  35. [ResponseType(typeof(void))]  
  36. public async Task<IHttpActionResult> PutEmployees(int id, Employee employees)  
  37. {  
  38. if (!ModelState.IsValid)  
  39. {  
  40. return BadRequest(ModelState);  
  41. }  
  42. if (id != employees.EmployeeID)  
  43. {  
  44. return BadRequest();  
  45. }  
  46. db.Entry(employees).State = EntityState.Modified;  
  47. try  
  48. {  
  49. await db.SaveChangesAsync();  
  50. }  
  51. catch (DbUpdateConcurrencyException)  
  52. {  
  53. if (!EmployeesExists(id))  
  54. {  
  55. return NotFound();  
  56. }  
  57. else  
  58. {  
  59. throw;  
  60. }  
  61. }  
  62. return StatusCode(HttpStatusCode.NoContent);  
  63. }  
  64. // POST: api/Employees  
  65. [ResponseType(typeof(Employee))]  
  66. public async Task<IHttpActionResult> PostEmployees(Employee employees)  
  67. {  
  68. if (!ModelState.IsValid)  
  69. {  
  70. return BadRequest(ModelState);  
  71. }  
  72. db.Employees.Add(employees);  
  73. await db.SaveChangesAsync();  
  74. return CreatedAtRoute("DefaultApi"new { id = employees.EmployeeID }, employees);  
  75. }  
  76. // DELETE: api/Employees/5  
  77. [ResponseType(typeof(Employee))]  
  78. public async Task<IHttpActionResult> DeleteEmployees(int id)  
  79. {  
  80. Employee employees = await db.Employees.FindAsync(id);  
  81. if (employees == null)  
  82. {  
  83. return NotFound();  
  84. }  
  85. db.Employees.Remove(employees);  
  86. await db.SaveChangesAsync();  
  87. return Ok(employees);  
  88. }  
  89. protected override void Dispose(bool disposing)  
  90. {  
  91. if (disposing)  
  92. {  
  93. db.Dispose();  
  94. }  
  95. base.Dispose(disposing);  
  96. }  
  97. private bool EmployeesExists(int id)  
  98. {  
  99. return db.Employees.Count(e => e.EmployeeID == id) > 0;  
  100. }  
  101. [Route("getterritoriesbyid")]  
  102. public IEnumerable<EmployeeTerritory> GetEmployeeTerritoriesByEmployee(int id)  
  103. {  
  104. return (from et in db.EmployeeTerritories.AsEnumerable()  
  105. join t in db.Territories.AsEnumerable() on et.TerritoryID equals t.TerritoryID  
  106. where et.EmployeeID == id  
  107. orderby et.TerritoryID  
  108. select new EmployeeTerritory  
  109. {  
  110. EmployeeID = et.EmployeeID,  
  111. TerritoryID = et.TerritoryID,  
  112. TerritoryDescription = t.TerritoryDescription  
  113. });  
  114. }  
  115. }  
  116. }  

 

Module

  1. var employeeapp = angular.module('employeeapp', ['ui.grid''ui.grid.pagination',  
  2. 'ui.grid.selection''ui.grid.exporter',  
  3. 'ui.grid.grouping''ui.grid.expandable']);  

 

Service

  1. //Service  
  2. employeeapp.service("employeeservice", function ($http, $timeout) {  
  3. //Function to call get employee web api call  
  4. this.GetEmployee = function () {  
  5. var req = $http.get('api/employeeapi/get');  
  6. return req;  
  7. }  
  8. //function to get territories based on employeeid  
  9. this.GetTerritories = function (employeeId) {  
  10. var req = $http.get('api/employeeapi/getterritoriesbyid?id=' + employeeId);  
  11. return req;  
  12. }  
  13. });  

Controller

  1. //Controller  
  2. employeeapp.controller("employeegroupingcontroller", function ($scope, employeeservice, $filter, $window, $interval, uiGridGroupingConstants, $timeout) {  
  3. GetEmployee();  
  4. function GetEmployee() {  
  5. employeeservice.GetEmployee().then(function (result) {  
  6. $scope.Employees = result.data;  
  7. console.log($scope.Employees);  
  8. }, function (error) {  
  9. $window.alert('Oops! Something went wrong while fetching genre data.');  
  10. })  
  11. }  
  12. //Columns  
  13. $scope.columnDefs = [  
  14. { name: '', field: 'EmployeeID', enableColumnMenu: false },  
  15. { name: 'photo', enableSorting: false, field: 'PhotoPath', cellTemplate: "<img width=\"50px\" ng-src=\"{{grid.getCellValue(row, col)}}\" lazy-src>", enableCellEdit: false, enableFiltering: false, enableGrouping:false },  
  16. { name: 'First Name', field: 'FirstName', headerCellClass: 'tablesorter-header-inner', enableCellEdit: true, enableFiltering: true, enableGrouping:false },  
  17. { name: 'Last Name', field: 'LastName', headerCellClass: 'tablesorter-header-inner', enableCellEdit: true, enableFiltering: true, enableGrouping:false },  
  18. { name: 'Title', field: 'Title', headerCellClass: 'tablesorter-header-inner', enableCellEdit: true, enableFiltering: false, enableGrouping:false },  
  19. { name: 'City', field: 'City', headerCellClass: 'tablesorter-header-inner', enableCellEdit: true, enableFiltering: true, enableGrouping:false },  
  20. { name: 'Country', field: 'Country', headerCellClass: 'tablesorter-header-inner', enableCellEdit: true, enableFiltering: true, enableGrouping:false },  
  21. { name: 'Notes', field: 'Notes', headerCellClass: 'tablesorter-header-inner', enableCellEdit: true, enableFiltering: false, enableGrouping:false },  
  22. { name: 'Salary', field: 'Salary', headerCellClass: 'tablesorter-header-inner', enableCellEdit: true, enableFiltering: false, enableGrouping:true }  
  23. ];  
  24. //Used to bind ui-grid  
  25. //$scope.selectedItem = null;  
  26. $scope.gridOptions = {  
  27. //For inline filter  
  28. enableGridMenu: false,  
  29. enableRowSelection: true,  
  30. enableRowHeaderSelection: false,  
  31. paginationPageSizes: [5, 10, 20, 30, 40],  
  32. paginationPageSize: 10,  
  33. enableSorting: true,  
  34. exporterMenuPdf: false,  
  35. enableFiltering: false,  
  36. treeRowHeaderAlwaysVisible: false,  
  37. //This code used for export grid data in csv file  
  38. exporterCsvFilename: 'Test_' + $filter('date')(new Date(), 'MM/dd/yyyy') + '.csv',  
  39. exporterCsvLinkElement: angular.element(document.querySelectorAll(".custom-csv-link-location")),  
  40. // This code used to expand the grid  
  41. expandableRowTemplate: '<div ui-grid="row.entity.subGridOptions" style="height:140px;" ></div>',  
  42. //expandableRowHeight: 150,  
  43. //subGridVariable will be available in subGrid scope  
  44. expandableRowScope: {  
  45. subGridVariable: 'subGridScopeVariable'  
  46. },  
  47. //End here  
  48. onRegisterApi: function (gridApi) {  
  49. $scope.gridApi = gridApi;  
  50. gridApi.expandable.on.rowExpandedStateChanged($scope, function (row) {  
  51. if (row.isExpanded) {  
  52. row.entity.subGridOptions = {  
  53. columnDefs: [  
  54. { name: 'Employee Id', field:'EmployeeID'},  
  55. { name: 'Territory Description', field:'TerritoryDescription'}  
  56. ]};  
  57. employeeservice.GetTerritories(row.entity.EmployeeID).then(function (data) {  
  58. row.entity.subGridOptions.data = data.data;  
  59. });  
  60. }  
  61. });  
  62. },  
  63. //end here  
  64. columnDefs: $scope.columnDefs,  
  65. //data for grid  
  66. data: 'Employees'  
  67. };  
  68. });  

To show the subgrid you need to provide following grid option:

  • gridOptions - Options for configuring the expandable feature, these are available to be set using the ui-grid gridOptions
  • publicApi - Public Api for expandable feature

Global

Now, add a few lines in Global.asax in Application_Start event.

  1. GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;  
  2. GlobalConfiguration.Configuration.Formatters.Remove(GlobalConfiguration.Configuration.Formatters.XmlFormatter);  

Now, add the mandatory packages given below, using NuGet Package Manager.


Bundling

Bundle the required styles and scripts.

Add the bundles given below in BundleConfig.cs

  1. bundles.Add(new ScriptBundle("~/bundles/uigrid").Include(  
  2. "~/Scripts/ui-grid.min.js"));  
  3. bundles.Add(new ScriptBundle("~/bundles/angular").Include(  
  4. "~/Scripts/angular.min.js",  
  5. "~/Angular/Controller/employeecontroller.js",  
  6. "~/Angular/Controller/employeegroupingcontroller.js"));  
  7. Render all the scripts and styles in _Loyout.cshtml  
  8. @Styles.Render("~/Content/css")  
  9. @Scripts.Render("~/bundles/modernizr")  
  10. @Scripts.Render("~/bundles/jquery")  
  11. @Scripts.Render("~/bundles/bootstrap")  
  12. @Scripts.Render("~/bundles/angular")  
  13. @Scripts.Render("~/bundles/uigrid")  
  14. @RenderSection("scripts", required: false)  

View

  1. @{  
  2. ViewBag.Title = "Index";  
  3. Layout = "~/Views/Shared/_Layout.cshtml";  
  4. }  
  5. <h2>Employee List</h2>  
  6. <div ng-app="employeeapp" ng-controller="employeegroupingcontroller">  
  7. <div ui-grid="gridOptions"  
  8. class="grid"  
  9. ui-grid-pagination  
  10. ui-grid-selection  
  11. ui-grid-auto-resize  
  12. ng-cloak  
  13. ui-grid-exporter  
  14. ui-grid-expandable>  
  15. </div>  
  16. </div>  

Output


In given screenshot as you can see first column has expandable icon. If you click on header expandable icon that will open or close all subgrids.


If you click on one icon that open a sub grid which has all territories for selected employee.


If you have more records then scroll bar should come in subgrid.

Conclusion

In this article, we have seen how to implement expandable in angular ui-grid with Web API with an Entity Framework in MVC. If you have any question or comments, drop me a line in the comments section.