Introduction
This article will guide you how to create a html table with collapsible verticle header using AngularJS in ASP.NET MVC.
Background
This html table is created using AngularJS "ng-repeat" directive and "ng-if" directive.
- The usage of ng-repeat is to:
Loop through the list of data and populate the table row.
- The usage of ng-if is to:
Removes the HTML element if the expression evaluates to false and vice-versa.
It also uses other AngularJS directive such as "ng-init", "ng-click", "ng-class" and "ng-cloak".
The table consists of a fixed header with a dynamically generated vertical table header with collapsible function and is follow by the dynamically generated table items.
* Note: Each dynamically generated table items will have same Id as the vertical table header Id (Parent Id).
Number of vertical table header and table items depends on the list of data retrieved from database.
Using the code
Set up the AngularJS environment in ASP.Net MVC:
- Import AngularJS library either by NuGet or manually download the library from this website.
For this example I will be setting up the environment manually.
Place the downloaded AngularJS library into Scripts folder:
Add the angular.min.js into BundleConfig:
using System.Web;
using System.Web.Optimization;
namespace AngularJSVerticalTable
{
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Scripts/jquery-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
"~/Scripts/jquery.validate*"));
bundles.Add(new ScriptBundle("~/bundles/angularjs").Include(
"~/Scripts/angular.min.js",
"~/Scripts/angular-ui/ui-bootstrap-tpls.min.js"));
bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
"~/Scripts/modernizr-*"));
bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
"~/Scripts/bootstrap.js",
"~/Scripts/respond.js"));
bundles.Add(new StyleBundle("~/Content/css").Include(
"~/Content/bootstrap.css",
"~/Content/site.css"));
}
}
}
In _Layout.cshtml, include the AngularJS bundle script, set up the ng-app and angular module.
All are commented and highlighted in Bold.
<!DOCTYPE html>
<!---->
<html ng-app="complianceApp">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@ViewBag.Title - My ASP.NET Application</title>
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
@Scripts.Render("~/bundles/jquery")
<!---->
@Scripts.Render("~/bundles/angularjs")
@Scripts.Render("~/bundles/bootstrap")
<!---->
<script>
angular.module("complianceApp", ["ui.bootstrap"]);
</script>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
@Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>@Html.ActionLink("Home", "Index", "Home")</li>
<li>@Html.ActionLink("About", "About", "Home")</li>
<li>@Html.ActionLink("Contact", "Contact", "Home")</li>
</ul>
</div>
</div>
</div>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© @DateTime.Now.Year - My ASP.NET Application</p>
</footer>
</div>
@RenderSection("scripts", required: false)
</body>
</html>
Create a Model and name it "ComplianceModels":
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace AngularJSVerticalTable.Models
{
public class ComplianceClause
{
public Guid? Id { get; set; }
public string Description { get; set; }
public double? Sequence { get; set; }
public string DisplaySequence { get; set; }
public int ClauseItemCount { get; set; } = 0;
}
public class ComplianceClauseItem
{
public Guid? Id { get; set; }
public Guid? ComplianceClauseId { get; set; }
public string TempComplianceClauseId { get; set; }
public string Description { get; set; }
public double? Sequence { get; set; }
public string DisplaySequence { get; set; }
}
public class ComplianceTemplateList
{
public List<ComplianceClause> ComplianceClauseList { get; set; }
public List<ComplianceClauseItem> ComplianceClauseItemList { get; set; }
public List<Compliance> listOfCompliance { get; set; }
}
public class Compliance
{
public bool IsHeader { get; set; }
public string SNo { get; set; }
public string Requirement { get; set; }
public Guid ComplianceClauseItemId { get; set; }
public Guid ClauseId { get; set; }
public int ComplianceClauseItemCount = 0;
}
}
Create a new General folder and add a class:
Add the codes below into the Repository class:
using AngularJSVerticalTable.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace AngularJSVerticalTable.General
{
public class Repository
{
public List<ComplianceClause> getComplianceClause()
{
List<ComplianceClause> complianceClauseList = new List<ComplianceClause>();
for (int i = 0; i < 3; i++)
{
ComplianceClause complianceClause = new ComplianceClause();
switch (i)
{
case 0:
complianceClause.Description = "Scope of Work 1: Secondary Data Research";
complianceClause.DisplaySequence = "";
complianceClause.Id = Guid.Parse("439d1083-3af8-4e1e-95ae-8667a8c8b462");
complianceClause.Sequence = 1;
break;
case 1:
complianceClause.Description = "Scope of Work 2: Analytical Workshops";
complianceClause.DisplaySequence = "";
complianceClause.Id = Guid.Parse("c6b71354-a81f-46b2-b841-53007e9eb236");
complianceClause.Sequence = 2;
break;
case 2:
complianceClause.Description = "Scope of Work 3: Compliance to Specific Requirement";
complianceClause.DisplaySequence = "";
complianceClause.Id = Guid.Parse("08ebdd75-7a88-471d-90b8-8d1bd4a86b48");
complianceClause.Sequence = 3;
break;
default:
complianceClause.Description = "";
complianceClause.DisplaySequence = "";
complianceClause.Id = Guid.Empty;
complianceClause.Sequence = 0;
break;
}
complianceClauseList.Add(complianceClause);
}
return complianceClauseList;
}
public List<ComplianceClauseItem> getComplianceClauseItem()
{
List<ComplianceClauseItem> complianceClauseItemList = new List<ComplianceClauseItem>();
for (int j = 0; j < 5; j++)
{
ComplianceClauseItem complianceClauseItem = new ComplianceClauseItem();
switch (j)
{
case 0:
complianceClauseItem.Description = "The company must be registered with Microsoft and a Certified Microsoft System Integrator (Please submit together copy of the certificate)";
complianceClauseItem.DisplaySequence = "3.1";
complianceClauseItem.Id = Guid.Parse("9916f981-2c05-486c-aa89-41b51473c4f4");
complianceClauseItem.Sequence = 1;
complianceClauseItem.ComplianceClauseId = Guid.Parse("08ebdd75-7a88-471d-90b8-8d1bd4a86b48");
complianceClauseItem.TempComplianceClauseId = string.Empty;
break;
case 1:
complianceClauseItem.Description = "Data gathering and analysis from sources obtained by RO";
complianceClauseItem.DisplaySequence = "1.1";
complianceClauseItem.Id = Guid.Parse("e08b4eea-acfa-4ab6-9a9f-9586207758d8");
complianceClauseItem.Sequence = 1;
complianceClauseItem.ComplianceClauseId = Guid.Parse("439d1083-3af8-4e1e-95ae-8667a8c8b462");
complianceClauseItem.TempComplianceClauseId = string.Empty;
break;
case 2:
complianceClauseItem.Description = "Data gathering and analysis from sources obtained by MDEC";
complianceClauseItem.DisplaySequence = "1.2";
complianceClauseItem.Id = Guid.Parse("0fe915f2-9061-4b86-a44d-6bb4781e4d16");
complianceClauseItem.Sequence = 2;
complianceClauseItem.ComplianceClauseId = Guid.Parse("439d1083-3af8-4e1e-95ae-8667a8c8b462");
complianceClauseItem.TempComplianceClauseId = string.Empty;
break;
case 3:
complianceClauseItem.Description = "Expert Group: Macroeconomic Workshop and Findings";
complianceClauseItem.DisplaySequence = "2.1";
complianceClauseItem.Id = Guid.Parse("92d0d4aa-299d-42d5-b098-f9bdae12be78");
complianceClauseItem.Sequence = 1;
complianceClauseItem.ComplianceClauseId = Guid.Parse("c6b71354-a81f-46b2-b841-53007e9eb236");
complianceClauseItem.TempComplianceClauseId = string.Empty;
break;
case 4:
complianceClauseItem.Description = "Expert Group: Industry Workshop and Findings";
complianceClauseItem.DisplaySequence = "2.2";
complianceClauseItem.Id = Guid.Parse("e8b80038-1e99-47c9-9e2c-7dd16b19f622");
complianceClauseItem.Sequence = 2;
complianceClauseItem.ComplianceClauseId = Guid.Parse("c6b71354-a81f-46b2-b841-53007e9eb236");
complianceClauseItem.TempComplianceClauseId = string.Empty;
break;
default:
complianceClauseItem.Description = "";
complianceClauseItem.DisplaySequence = "";
complianceClauseItem.Id = Guid.Empty;
complianceClauseItem.Sequence = 0;
complianceClauseItem.ComplianceClauseId = Guid.Parse("");
complianceClauseItem.TempComplianceClauseId = string.Empty;
break;
}
complianceClauseItemList.Add(complianceClauseItem);
}
return complianceClauseItemList;
}
public List<Compliance> getListOfCompliance(List<ComplianceClause> complianceClauseList, List<ComplianceClauseItem> complianceClauseItemList)
{
List<Compliance> listOfCompliance = new List<Compliance>();
int sNo = 0;
foreach (ComplianceClause cm in complianceClauseList)
{
Compliance clauseCompliance = new Compliance();
clauseCompliance.IsHeader = true;
clauseCompliance.SNo = cm.Sequence.ToString();
clauseCompliance.Requirement = cm.Description;
clauseCompliance.ClauseId = cm.Id.HasValue ? cm.Id.Value : Guid.Empty;
listOfCompliance.Add(clauseCompliance);
double subHeaderSequence = sNo;
foreach (ComplianceClauseItem im in complianceClauseItemList)
{
if (im.ComplianceClauseId == cm.Id)
{
Compliance itemsCompliance = new Compliance();
itemsCompliance.IsHeader = false;
itemsCompliance.SNo = im.DisplaySequence;
itemsCompliance.Requirement = im.Description;
itemsCompliance.ComplianceClauseItemId = im.Id.HasValue ? im.Id.Value : Guid.Empty;
itemsCompliance.ClauseId = cm.Id.HasValue ? cm.Id.Value : Guid.Empty;
listOfCompliance.Add(itemsCompliance);
}
}
}
foreach (var item in listOfCompliance)
{
if (item.IsHeader)
{
item.ComplianceClauseItemCount = listOfCompliance.Where(a => a.ClauseId == item.ClauseId && a.IsHeader == false).Count();
}
}
return listOfCompliance;
}
}
}
* Note: For simplicity I'll use hardcoded list as an example, however in actual usage it could be retrieve from a database.
The Repository class can be modify to accomodate the function to retrieve data from database.
In Home controller, create a new page and name it "VerticalTable":
public ActionResult VerticalTable()
{
Repository repository = new Repository();
ComplianceTemplateList complianceTemplateList = new ComplianceTemplateList();
complianceTemplateList.ComplianceClauseList = repository.getComplianceClause();
complianceTemplateList.ComplianceClauseItemList = repository.getComplianceClauseItem();
List<Compliance> listOfCompliance = repository.getListOfCompliance(complianceTemplateList.ComplianceClauseList, complianceTemplateList.ComplianceClauseItemList);
complianceTemplateList.listOfCompliance = listOfCompliance;
return View(complianceTemplateList);
}
}
Create a view from the VerticleTable controller:
@model AngularJSVerticalTable.Models.ComplianceTemplateList
@{
ViewBag.Title = "Vertical Table";
}
<style type="text/css">
.complianceHeader {
background-color: #333;
color: rgba(255,255,255,0.87);
}
</style>
<h2>Vertical Table</h2>
<div ng-controller="complianceCtrl" ng-cloak>
<br>
<div class="table-responsive table-Item">
<table class="table table-bordered">
<thead>
<tr>
<th class="complianceHeader" style="text-align:center;" width="5%">No.</th>
<th class="complianceHeader" width="95%">Requirement</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="idy in ComplianceList track by $index">
<th colspan="2" class="complianceHeader"
ng-if="idy.IsHeader"
ng-init="initHeader(idy.ClauseId, idy.TenderComplianceClauseItemCount)">
{{idy.SNo}}. {{idy.Requirement}}
<a href="javascript:void(0)" ng-click="ToggleHeader(idy.ClauseId)"><span ng-class="externalHeaderClassFunction(idy.ClauseId, idy.TenderComplianceClauseItemCount)" style="color:white;"></span></a>
</th>
<td style="text-align:center;"
ng-if="externalHeaderFunction(idy.IsHeader, idy.ClauseId)">
<p>{{idy.SNo}}</p>
</td>
<td ng-if="externalHeaderFunction(idy.IsHeader, idy.ClauseId)">
<p>{{idy.Requirement}}</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<script type="text/javascript">
(function ($) {
'use strict';
angular.module("complianceApp").controller("complianceCtrl", ["$scope", "complianceService", function ($scope, complianceService) {
$scope.ComplianceList = complianceService.getComplianceList();
$scope.collapsedHeader = [];
$scope.externalHeaderClassFunction = function(data, itemCount){
let arrayIndex = $scope.collapsedHeader.map(function (e) { return e.id; }).indexOf(data);
if(arrayIndex !== -1){
if($scope.collapsedHeader[arrayIndex].value===false){
if(itemCount > 0)
return "glyphicon glyphicon-minus pull-right";
else
return "";
}
else{
return "glyphicon glyphicon-plus pull-right";
}
}else{
return "glyphicon glyphicon-plus pull-right";
}
}
$scope.externalHeaderFunction = function(IsHeader, data){
let arrayIndex = $scope.collapsedHeader.map(function (e) { return e.id; }).indexOf(data);
if(arrayIndex !== -1){
if(IsHeader===false && $scope.collapsedHeader[arrayIndex].value===false){
return true;
}
else{
return false;
}
}else{
return false;
}
}
$scope.initHeader = function(sNo, itemCount){
let objHeader = {
id: sNo,
value: false,
TenderComplianceClauseItemCount: itemCount
}
$scope.collapsedHeader.push(objHeader);
}
$scope.ToggleHeader = function(Id){
let arrayIndex = $scope.collapsedHeader.map(function (e) { return e.id; }).indexOf(Id);
if(arrayIndex !== -1){
$scope.collapsedHeader[arrayIndex].value = !$scope.collapsedHeader[arrayIndex].value;
}
}
}]);
angular.module("complianceApp").factory("complianceService", ["$http", function ($http) {
let service = {};
let complianceList = @Html.Raw(Json.Encode(@Model));
service.getComplianceList = function(){
return complianceList.listOfCompliance;
}
return service;
}]);
})(jQuery);
</script>
Testing the App
Compiled and execute the web application.
Points of Interest
It is quite difficult to find a suitable example to implement a html table with vertical headers and collapsible function and this pushes me to find a solution on my own.
Hope you will find this article useful!
History
- 13th January, 2017: First version
- 14th January, 2017: Second version