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.

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 what, where 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.

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!

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.

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).

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):

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

Share

About the Author

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

HI Simon - It runs in a mobile environment but other than seeing it run on my mobile I haven't done much testing!
I have build a much wider scoped version in React that I now use at work but simply wanted to demonstrate that sometimes creating your own controls really can be worth it to gain ultimate control over look and feel.

I think that could be possible - I've build a main grid here at work along this articles' principles where I do expose a method 'FindAndSelect' that finds a row in the entire datasource and ensures the grid selects and scrolls to that row (it also selects the correct page!).

For now though I believe there might be other parts of functionality that people might want like Filters, remote datasources etc.

Not pressing, but - playing with the demo - it struck me as the first thing that it shouldn't be possible to watch half a line at the top.

I think this should take priority. When I think about it, I can't recall to have seen a grid or table that didn't disply a full record at top.
And this can't be taken care of otherwise; filtering, for example, can be handled in several other ways as you mention.

Sorry Gustav - after several close examinations of the Tags lister popup (in both 'Language' and 'Technology' lists) I honestly still cannot find the 'Typescript' tag. Perhaps it takes some time to filter through the system so I will keep my eye on it..

Thanks Richard - I realise that but as I mentioned in the article, when showing code where I concatenate strings using angled brackets ('<' and '>') I can't use the usual code blocks. I already tried your < pre suggestion but CP's auto formatting messes up the code completely..