Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Collapsible vertical table headers in AngularJS + ASP.NET MVC

0.00/5 (No votes)
13 Jan 2017 1  
This article talks about how to create a html table with collapsible vertical headers using AngularJS in ASP.Net MVC web application.

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
    {
        // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862
        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*"));
            
            // AngularJS library
            bundles.Add(new ScriptBundle("~/bundles/angularjs").Include(
                       "~/Scripts/angular.min.js",
                       "~/Scripts/angular-ui/ui-bootstrap-tpls.min.js"));

            // Use the development version of Modernizr to develop with and learn from. Then, when you're
            // ready for production, use the build tool at http://modernizr.com to pick only the tests you need.
            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>
<!-- Angular JS ng-app-->
<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")

    <!-- Angular JS library -->
    @Scripts.Render("~/bundles/angularjs")

    @Scripts.Render("~/bundles/bootstrap")

    <!-- Angular JS module -->
    <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>&copy; @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>();
            
            // Hardcoded list as an example, in actual usage it should be retrieve from a database.
            // Generates table headers with a unique Id
            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>();

            // Hardcoded list as an example, in actual usage it should be retrieve from a database.
            // Generates table items with a unique Id and is set to a table header Id (ComplianceClauseId)
            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;

            // For all the items in compliance clause list, "IsHeader" is set to true
            foreach (ComplianceClause cm in complianceClauseList)
            {
                Compliance clauseCompliance = new Compliance();
                clauseCompliance.IsHeader = true;
                clauseCompliance.SNo = cm.Sequence.ToString();
                clauseCompliance.Requirement = cm.Description;
                
                // Collapsible compliance
                clauseCompliance.ClauseId = cm.Id.HasValue ? cm.Id.Value : Guid.Empty;

                listOfCompliance.Add(clauseCompliance);

                double subHeaderSequence = sNo;
                
                // Iterates through all the compliance Clause Item List and set the "IsHeader"
                // to false and added into listOfCompliance if the ComplianceClauseId is 
                // match with the ComplianceClauseItem Id
                foreach (ComplianceClauseItem im in complianceClauseItemList)
                {
                    if (im.ComplianceClauseId == cm.Id)
                    {
                        Compliance itemsCompliance = new Compliance();
                        itemsCompliance.IsHeader = false;
                        // Sub section
                        itemsCompliance.SNo = im.DisplaySequence;
                        itemsCompliance.Requirement = im.Description;
                        itemsCompliance.ComplianceClauseItemId = im.Id.HasValue ? im.Id.Value : Guid.Empty;

                        // Collapsible compliance
                        itemsCompliance.ClauseId = cm.Id.HasValue ? cm.Id.Value : Guid.Empty;

                        listOfCompliance.Add(itemsCompliance);
                    }
                }
            }

            // Counts the number of tender compliance clause item
            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":

//
// Controller for 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) {   
            // Retrieve the compliance list from the compliance service
            // This part gets the list of data from the server and assigns to "ComplianceList"
            $scope.ComplianceList = complianceService.getComplianceList();
            
            // Array that stores the id and status of the compliance header and compliance items
            $scope.collapsedHeader = [];

            // Function to determine whether to change the CSS of the header class based on the parameter (collapsed / expanded)
            $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";
                }
            }

            // Function to determine whether to add the compliance item based on the parameter
            // Compliance item is added if return value is true
            $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;
                }
            }

            // Initialize compliance header and populate the "collapsedHeader" array
            $scope.initHeader = function(sNo, itemCount){
                let objHeader = {
                    id: sNo,
                    value: false,
                    TenderComplianceClauseItemCount: itemCount
                }

                $scope.collapsedHeader.push(objHeader);
            }

            // Function to change the status of the header (expand / collapse)
            $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 = {};

            // Retrieve the compliance list from the Compliance Models
            let complianceList = @Html.Raw(Json.Encode(@Model));
            
            // Return the compliance list to the angular controller
            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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here