Click here to Skip to main content
12,816,775 members (33,904 online)
Click here to Skip to main content
Add your own
alternative version

Stats

33.8K views
1.6K downloads
55 bookmarked
Posted 7 Jun 2015

A lightweight HTML5 grid from scratch

, 19 Mar 2017 CPOL
Rate this:
Please Sign up or sign in to vote.
An introduction as to how to create a semi-advanced extendible HTML5 grid from scratch. This really isn't as daunting a task as many think!

See the grid in action here

Introduction

This article describes how to create an HTML5 grid from scratch.  It explores a technique where the grid skeleton is created with placeholders that are then populated on demand.  The advantage of writing one yourself is that you are in full control of what your grid is capable of. If anything needs changing or adding then this can be done in a matter of hours rather than days as I found out our over the last few years.

Have a closer 'live' look at the grid here.  Source code is available on GitHub as BlueSkyGrid. 

Background

The idea of building your own control(s) is a hotly contested one. Especially a grid which is perceived as a complex control falls in this category.  

Whilst it's true that buying a ready made grid gets you a lot of functionality 'out of the box' it does come at a price. We found that if there is anything you want that is not provided by the grid natively you are stuck. Yes, one can buy the source code and try to extend it but that is often not that easy. It also needs to be managed when new versions or updates are released by the vendor.

Don't get me wrong, there are advantages to buying a grid off the shelf, it can be up and running quickly and it probably plays well with some other components (from the same vendor though).

So if you are willing to compromise then buying a grid might be for you.

However, as I will demonstrate in this article, writing your own grid is actually not as hard as you might think.  On top of that you get exactly what you want and if anything isn't working you can find and fix it quickly yourself rather than endlessly checking the vendors FAQ's and forums where people may have 'work-rounds' which can break when new versions are released.  

Grid Features

The grid currently provides the following features:

  • Column resizing,
  • Column reordering,
  • Sorting,
  • Paging,
  • Flexible Column - a single column that fills up any white space in the grid. (resize the flexible column to disable it from flexing, simply double click on the column resizer to activate this feature again!)
  • Currencies

It currently lacks:

  • Filters (however, one can restrict the data given to the grid for now!)

Create your first grid

You have a DOM element that you wish to insert a grid into.  So first define a set of Column Definitions to represent the data you wish to display.  The ColDefs inform the grid whatwhere and how to display it's data. Note that more data can be given but simply won't be shown if no matching ColDef is found.  We also define (optionally) some CurrencyInfo objects so the grid will have a way to lookup any currency symbols for fields that may require this. 

The code below is defined in your client code from where you create and manage your grid.

// one-off, define some currencies for the CurrencyManager so it can return symbols for the grid
__cm.setCurrencyInfo(new __cm.CurrencyInfo("GBP", "British Pound", "£", "GBP", ""));
__cm.setCurrencyInfo(new __cm.CurrencyInfo("USD", "US Dollar", "$", "USD", ""));
__cm.setCurrencyInfo(new __cm.CurrencyInfo("EUR", "Euro", "€", "EUR", ""));

// define the column definitions for the grid
this._coldefs = [];
this._coldefs.push(new __gc.ColDefinition("code", "Code", 100, "", "", "", "asc"));
this._coldefs.push(new __gc.ColDefinition("fullname", "Fullname", -1));     
this._coldefs.push(new __gc.ColDefinition("county", "County", 110, "", "", "", "", "", true));
this._coldefs.push(new __gc.ColDefinition("currency", "Currency", 90, "", "", "center"));
this._coldefs.push(new __gc.ColDefinition("valuation", "Valuation", 110, "number", "0,0.00", "right", "", "currency"));
this._coldefs.push(new __gc.ColDefinition("price", "Price", 110, "number", "0,0.00", "right", "", "currency"));
this._coldefs.push(new __gc.ColDefinition("myimage", "Img", 50, "image", "", "center"));
this._coldefs.push(new __gc.ColDefinition("created", "Created", 150, "date", "", "center"));

Let's take a closer look at the 'Price' row.  This row defines a ColDefinition for a property called 'price' on the given data rows.  In this case we specify that the header should display 'Price', have a width of 110 pixels, tell the column it is numeric and have formatting applied ('0,0.00') through the use of numeral.js.  If the field was of type 'date' it calls on moment.js for its formatting. The column should also be right aligned and look at the 'currency' data-field for its currency sign.   Supplying a currency column lookup will force the grid to look-up the equivalent sign and display it.

Now that we have a set of ColDefinitions we find the DOM element to place the grid within, create a GridController and ask for the grid skeleton to be created (as shown in the code snippet below).

Now the grid is fully created and ready to accept data we create some sample data and hand this over to the grid instance (together with the coldefs).

Note that in this example we supply the ColDefs array together with the data itself after the grid is created. This shows that you can send in different data at any time as long as it is accompanied by a set of matching ColDefs!

// find the element we will place the grid in
var $grid = $('.mygrid');

// create a grid controller
this._gc = new __gc.GridController();
this._gc.createGrid($grid);

// create some sample data
var data = __data.generateSampleData(rowcount);

// simply pass on the data (and their definitions) to the grid
this._gc.setData(data, this._coldefs).done(function () {
    ...
});

...

That's all there is to it to have the grid placed in your placeholder element.  

Note that the call to setData returns a jQuery promise informing the caller when the creation of the grid is complete. 

The code

There is too much code to describe the grid code in detail so in this article I will discuss the core principles and highlight some code parts that are of key interest.  I recommend copying the zip file or check out this code on github for a deeper understanding.

Note that I come from a C# background and therefore have embraced Typescript like a long lost child. It allows me to stay in an OO thinking mode, easily derive from other classes or implement interfaces, have type safety and tons of other good stuff!

As mentioned, the full source code can be found on GitHub and a working example in all its glory can be seen on my far from finished blog site. I will edit and enhance this grid further over time if there is enough interest.

Grid Construction

The core idea of creating an instance of this grid is that it is constructed in code and injected into a target div using the 'createGridStructure' function. This function builds up a string in code which gets converted into a DOM element (using jQuery) which is then injected into the awaiting target element.

Handles to the $header and $datarows sections are then obtained after which these sections are populated. Again, first in code then injected into each of their placeholders (createHeader and createRows respectively).  

The grid is based on three pieces of information, the actual data, an array of Column Definitions describing the columns that show this data and the target DOM element that we will inject the finished grid into.

The Column Definitions describe each column in terms of what data field to use from the datarow, the header, formatting, alignment, it's type (string, number, date, image etc) and more.

An overview of the core classes used to generate this grid are shown below:

 

 

DataEngine.ts

An instance of the DataEngine is created to separate the concerns of controlling grid actions and data manipulation. The job of the DataEngine is to hold onto the original data and manage a set of perpared data that is called upon by the Controller when it builds up the rows.  This can be extended later to include a pipeline of filtering, grouping and/or editing of data.

GridController.ts

The GridController.ts focusses on creating all HTML that is injected into the given DOM element from scratch. This means that all these rows are recreated on re-sorting and column-reorder.  On Column-resize all effected cells are adjusted in code so is faster.  Re-creating all rows is not the end of the world as I found (1000 rows is still sub-zero seconds)

CurrencyMananger.ts

When a ColDefinition identifies a currency lookup field the currencymanager is presented with the value of that field (like 'USD') and returns its symbol ('$') which is then used as part of the formatting of the cell.

 

All grid cells within a single row are placed next to one another using css flex-boxes. I've put together some useful css classes calling on css-flexboxes that I use in many places where I have several div's within a parent div and one of these div's needs to fill up any available space.

Check the example below - there is a parent div (that can vary in height) which contains two fixed height div's on top and one at the bottom, I then want any remainder to be filled up. All there is to make this work is to set the parent class to 'flex-parent-col' and have one of the children set it's class to 'flex-child', that's all.  There is a 'flex-parent-row' for the row equivalent.

The grid-row is also build upon this structure.  The row itself is decorated with a class 'flex-parent-row' and one of its children will have set its class to 'flex-child' to fill up available space.

GridController.ts

All the following code samples are held within the GridController.ts

Below is the code that shows the creation of the core structure of the grid. It produces a string that is then converted to a DOM element and injected into the relevant placeholder.

private createGridStructure(showBorder: boolean): string {
    var s: string = "";

    // --------------------
    // define the grid structure
    // --------------------
    var s: string = "";

    s += "<div class='bs-grid flex-parent-col " + (showBorder ? "border" : "") + "' >";

    s += "  <div class='spinner' style='display: none'}> <div> <i class='fa fa-spinner fa-spin fa-3x'></i> </div> </div>"

    s += "  <div class='header-containment'> </div>"

    s += "  <div class='header' >"
    s += "      <div class='resize-marker' title='resize this column or double-click to make it the flex-column'> </div>"
    s += "      <div class='insert-marker'>";
    s += "          <i class='down fa fa-caret-down fa-2x'></i>";
    s += "      </div>";
    s += "      <div class='header-template' style='position: relative'> </div>"
    s += "  </div>";

    s += "  <div class='data-scrollable flex-child flex-scrollable' >";
    s += "      <div class='data'> </div>";
    s += "      <div class='full-resize-marker'> </div>"
    s += "  </div> "

    s += "  " + this.createPager();

    s += "</div>";

    return s;
}

private createPager(): string {
    var s: string = "";

    s += "      <div class='pager'>"

    s += "          <button type='button' class='btnFirstPage pager-button'> <i class='fa fa-step-backward' title='First page'></i> </button> ";
    s += "          <button type='button' class='btnPrevPage pager-button'> <i class='fa fa-caret-left fa-lg' title='Previous page'></i> </button> ";
    s += "          <span class='pager-text' style='margin: 0px 4px 0px 4px'></span>";
    s += "          <button type='button' class='btnNextPage pager-button'> <i class='fa fa-caret-right fa-lg' title='Next page'></i> </button> ";
    s += "          <button type='button' class='btnLastPage pager-button'> <i class='fa fa-step-forward' title='Last page'></i> </button> ";

    s += "          <span style='margin: 0px 2px 0px 20px'>page size: </span>";
    s += "          <select class=page-size title='Number of rows per page'></select>";

    return s;
}

And is called as such:

...

// create a grid string
var grid = this.createGridStructure(showBorder);

// create a DOM grid 
this.$grid = $(grid);

// add this grid to the given dom element
this.$grid.appendTo($el);

// get a handle to some important data parts
this.$header = $('.header-template', this.$grid);
this.$datarows = $('.data', this.$grid);
this.$pager = $('.pager', this.$grid);
this.$resizemarker = $('.resize-marker', this.$grid);
this.$resizeline = $('.full-resize-marker', this.$grid);
this.$insertmarker = $('.insert-marker', this.$grid);
this.$sgloading = $('.spinner', this.$grid);
this.$datascrollable = $('.data-scrollable', this.$grid);

...

So at first we call 'createGridStructure' which returns the entire make up of the grid as a string.  We then convert this string into a jQuery DOM element and append it to the given DOM element.

Voila, our grid has structure and is placed in the client DOM tree.

We then get handles on some important element like the header, the data-area, resizemarker and various other elements that we wish to control.  Remember that at this point we still only have the skeleton outlay so let's create the header and rows sections now.

The creation of the header and datarows follow the same pattern as the grid skeleton.  First we build up the string representing the complete header (or data-rows) then we create real DOM element(s) out of these and inject them into our placeholders ($header or $datarows respectively).

Let's have a look at the code constructing this header:

private createHeader(): string {

    var self = this;
    var headerTemplate: string = "<div class='row-header flex-parent-row' > ";

    $.each(this.colDefinitions, function (index, coldef: ColDefinition) {

        // define the css classes we apply to each header column
        var cssclasses: string = "cell-header cell-right-column ";
        cssclasses += coldef.classAlign + " ";
        if (coldef.isFlexCol) cssclasses += "stretchable ";
            
        // get and lowercase the colName allowing us to get hold of the appropriate column when taking actions like sorting, resizing etc.
        var colName = coldef.colName.toLowerCase();
        var hitem: string = "";
        hitem += "<div class='" + cssclasses + "' data-sgcol='" + colName + "' style='width: " + coldef.width + "px;'>";
        hitem += "<span>" + coldef.colHeader + "</span>";
        hitem += "<span class='" + self.getSortSymbol(coldef) + "'></span>";
        if (coldef.isFlexCol) 
            hitem += "<i class='pull-right fa fa-arrows-h' style='background-color: transparent; color: rgb(201, 201, 208);' title='this column is flexible sized'></i>";
        hitem += "</div>";
        headerTemplate += hitem;

    });
    headerTemplate += "</div>";
    return headerTemplate;
}

This is then called upon as such:

// create the header (string and DOM and append to placeholder)
var header = self.createHeader();

$(header).appendTo(self.$header);

And we have a header!

Then the same applies to the actual data rows:

private createRows(): string {
    var self = this;
    var s = "";

    for (var i = 0; i < self.dataEngine.baseDataPrepared.length; i++) {
        var dataItem: any = self.dataEngine.baseDataPrepared[i];

        // start row definition
        s += "      <div class='row flex-parent-row' data-pkvalue='" + dataItem.pkvalue + "' >";

        // step through each ColDefinition
        self.colDefinitions.forEach(function (coldef: ColDefinition) {

            // retrieve the actual raw cell value
            var myvalue = dataItem[coldef.colName];

            // check if any custom formatting is required
            myvalue = self.getFormattedValue(myvalue, dataItem, coldef);

            // ask the client to fill in any generic styling properties
            var styleProp: CellStyleProperies = self.cbStyling(coldef, dataItem);

            // start Cell definition
            s += "<div class='cell cell-right-column " + (coldef.isFlexCol ? "stretchable" : "") + " " + coldef.classAlign + "' ";
            s += "data-sgcol='" + coldef.colName + "' style='width: " + coldef.width + "px; ";

            // if a cell backcolour was given then apply it
            if (styleProp.cellBackColour) 
                s += " background-color: " + styleProp.cellBackColour + "; ";

            // if a cell forecolour was given then apply it
            if (styleProp.cellForeColour) 
                s += " color: " + styleProp.cellForeColour + "; ";

            s += "'>";  

            // check if this cell is merged with an image and we were given an image
            if (coldef.mergeWithImage && styleProp.imgName) 
                s += "<div class='merged-image " + coldef.colAlign + "' style='background-color: " + styleProp.imgBackColour + "; color: " + styleProp.imgForeColour + ";'> <i class='fa " + styleProp.imgName + "' > </i> </div>"

            // if this is an image colum then just show the image
            if (coldef.colType == "image") {
                var faImage = styleProp.imgName;  
                if (faImage.length == 0) faImage = myvalue;         // if no return value was given then use the actual data
                s += "<i class='fa " + faImage + "'\"></i>";
            }
            else
                // simply write out the actual cell value
                s += myvalue;

            s += " </div>"      // end cell definition

        });
        s += "      </div>";    // end row definition
    }

    return s;
}

And again is called like this:

// create all rows (string and DOM and append to placeholder)

var rows = self.createRows();

$(rows).appendTo(self.$datarows);

Few things to note here.  First, we iterate through our actual (prepared) dataitems and for each dataitem we iterate through the ColDefinitions array which then gives us enough information to define each individual cell.

When defining a cell we first push the raw value through a formatter that returns the formatted value. We then decorate the cell by assigning any appropriate classes as well as setting the width.

Cell Styling

To allow the client to define custom styling per cell we check if a custom styling function was given. If so, we call this with the current ColDef and the actual datarow allowing the client to make a decision having all available data at hand.

The idea is that for each cell the client creates a CellStyleProperties object that will be used when defining the cell by the GridController.  Properties that can be set include back and foreground colours, images and others.  

To see how custom styling of each cell works let's have a look at the following code (which is defined in the client code class):

this._gc.cbStyling = function (coldef: __gc.ColDefinition, item: any) {

    // define a new CellStyleProperties object we will return
    var styleProp: __gc.CellStyleProperies = new __gc.CellStyleProperies();

    // now check which column is being checked and set any style properties appropriately 
    if (coldef.colName == "county") {
        if (item["county"] == "Kent" && Math.floor(item["price"]) % 2 == 0)
            styleProp.cellBackColour = "rgb(178, 232, 178)";
        if (Math.floor(item["price"]) % 4 == 0) 
            styleProp.imgBackColour = "rgb(255, 196, 8)";
        if (item["county"] == "Sussex")
            styleProp.imgForeColour = "red";
    }

    // image
    if (coldef.colName == "myimage") {
        if (item["price"] < 250)
            styleProp.imgName = "fa-floppy-o";
        else if (item["price"] < 500)
            styleProp.imgName = "fa-warning";
    }

    ...
    ...

    return styleProp;
};

The function is called for each cell as they are constructed allowing us a high degree of customisation.

Grid Images

You may have noticed that my preferred way of getting images into the grid is by using font-awesome images. Since these work by decorating your elements with specific classes they are ideal for a grid constructed in code. 

A word on Event handling

After we have created the grid skeleton and have obtained its placeholders we attach handlers to some of these. These include click handlers for the navigation buttons, row click and double click and many more.

The above mentioned event handlers are connected as a 'one-off' since the skeleton if not recreated. The grid-header is different in that it is recreated frequently. Therefore the code that attaches the draggable and droppable jQuery-ui interactions allowing us to move columns is placed in a separate function and is called each time the header is created.

Selecting a row

In order to support the ability to select a row the DataEngine ensures that a property called 'pkvalue' is attached to each generated row.  This unique value is placed in each DOM row with a data-attribute called 'data-pkvalue' so that when a row is clicked jQuery can easily identify which row this was and add a 'selected' class to the row element.

Ajax

In this article the grid received all data which is held and controlled by the DataEngine from the start.  Even though javascript seems perfectly capable holding on and processing large amounts of data clearly there is nothing stopping you extending this DataEngine and call back to the server for paged data.  Just ensure you use a JQueryPromise<T> as a return object from the 'refresh' call to the Engine and adjust your code accordingly.

Closing word

This article has hopefully demonstrated that creating your own grid is a do-able proposition.  With the grid in its current state it really is not difficult to add more functionality.  If there is enough interest I will enhance the grid to expose functionality such as filtering, formula columns and perhaps ajax calls?

For my employer I have created a similar grid that has many more features that uses Knockout bindings for many properties and functions like populating cell content, width, order, dynamic updates to the data and much more. Here I tried to stay clear of knockout bindings to keep things simple and have everything done through code.

Please do have a look at the GitHub repository where the source code is held and feel free to fork the code and/or contribute to the project!

Points of interest

At first I was sceptical to what extend I could push the browser and JavaScript to callback that many times and to do that much DOM maniupulations on the fly but suffice to say that JS and the compilers/optimisers seem more than capable of handling this! One has to give the JavaScript compilers and optimisers much credit for this!!

History

vs 1.0 - initial release.

vs 1.1 - update to domain reference of demo grid

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

marcel heeremans
Software Developer (Senior)
United Kingdom United Kingdom
Ever since my dad bought me a Commodore 64 (some years back) I have been hooked to programming. For most of my working career I have worked intensely with C# (WinForms and WPF) but a few years back I started to investigate browser technologies and frameworks which are much more powerful than I thought at first!

I studied International Marketing but found the IT sector much more challenging. So straight from uni I took on some IT contract work in London in a buoyant market and never looked back.

If you wish to contact me then please do so on heeremans.marcel@gmail.com

You may also be interested in...

Comments and Discussions

 
QuestionBrowser compatibility? Pin
Simon Gulliver20-Mar-17 3:16
professionalSimon Gulliver20-Mar-17 3:16 
GeneralMy vote of 4 Pin
Muhammad Shahid Farooq19-Mar-17 20:26
professionalMuhammad Shahid Farooq19-Mar-17 20:26 
QuestionScrolling Pin
Gustav Brock21-Jul-15 9:31
memberGustav Brock21-Jul-15 9:31 
AnswerRe: Scrolling Pin
marcel heeremans21-Jul-15 21:33
membermarcel heeremans21-Jul-15 21:33 
GeneralRe: Scrolling Pin
Gustav Brock21-Jul-15 23:51
memberGustav Brock21-Jul-15 23:51 
SuggestionVery nice Pin
Gustav Brock21-Jul-15 9:29
memberGustav Brock21-Jul-15 9:29 
GeneralRe: Very nice Pin
marcel heeremans21-Jul-15 21:23
membermarcel heeremans21-Jul-15 21:23 
GeneralRe: Very nice Pin
Gustav Brock21-Jul-15 23:54
memberGustav Brock21-Jul-15 23:54 
NewsRe: Very nice Pin
Gustav Brock25-Jul-15 23:30
memberGustav Brock25-Jul-15 23:30 
GeneralRe: Very nice Pin
marcel heeremans26-Jul-15 23:02
membermarcel heeremans26-Jul-15 23:02 
GeneralMy vote of 5 Pin
Jonathan Lindkvist21-Jul-15 3:33
memberJonathan Lindkvist21-Jul-15 3:33 
GeneralRe: My vote of 5 Pin
marcel heeremans21-Jul-15 21:34
membermarcel heeremans21-Jul-15 21:34 
QuestionNeeds formatting Pin
Richard MacCutchan14-Jul-15 0:47
protectorRichard MacCutchan14-Jul-15 0:47 
AnswerRe: Needs formatting Pin
marcel heeremans14-Jul-15 1:13
membermarcel heeremans14-Jul-15 1:13 
GeneralRe: Needs formatting Pin
Richard MacCutchan14-Jul-15 1:40
protectorRichard MacCutchan14-Jul-15 1:40 
GeneralRe: Needs formatting Pin
ProgramFOX14-Jul-15 5:16
mentorProgramFOX14-Jul-15 5:16 
GeneralRe: Needs formatting Pin
marcel heeremans19-Jul-15 22:56
membermarcel heeremans19-Jul-15 22:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.170308.1 | Last Updated 19 Mar 2017
Article Copyright 2015 by marcel heeremans
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid