Making a new datatable for the web
I was on a mission to remove all the jQuery UI components we used in our application and replace them with lightweight solutions. The only component left to replace was our table component — SlickGrid.
I thought I’d share my experience of building an alternative solution.
SlickGrid was great and fast. But it was no longer maintained and also the UI was dated. We searched for many data table libraries. But either some were not up to our taste or were restricting in terms of license. So I started building one from scratch.
Initially, it was a part of the application itself. I used jQuery to build it, since we were already using it in our application. Everything was great until I decided to move it into it’s own separate library.
You dont need jQuery
The first thing was to remove jQuery. After going through the codebase, it was surprising how little of jQuery I was using. Mostly it was for querying elements and setting innerHTML of an element. It was not very difficult to replace those with the native counterparts the DOM API already has: querySelector
and querySelectorAll
.
As I kept removing parts which used jQuery, I realised not everything is provided as a convenience in the native API. I had to create elements dynamically, bind/unbind events on them, access data attributes, set styles and much more. Then I came across PlainJS which listed plain JS alternatives to methods provided by jQuery. I started building a “small” subset of utilities as I required them. It was a great exercise for me to learn how the internals work.
The following is an implementation of jQuery’s closest
method which finds the first parent up the DOM Tree.
function closest(selector, element) {
if (!element) return null; if (element.matches(selector)) {
return element;
} return closest(selector, element.parentNode);
};
Another useful utility is to add delegated DOM events. It is a great pattern when you have lots of DOM Elements sharing the same event handler. Here is the native implementation:
function delegate(element, event, selector, callback) {
element.addEventListener(event, function (e) {
const delegatedTarget = closest(selector, e.target);
if (delegatedTarget) {
e.delegatedTarget = delegatedTarget;
callback.call(this, e, delegatedTarget);
}
});
}// Usage
delegate(parentElement, 'click', '.dt-cell', focusCell);
What the heck is the Event Loop?
I was working on a feature where when you drag the column header width, the corresponding cells in the same column should also adapt. In my first naive implementation, I just updated the width directly on each cell on EVERY drag event. It was pretty bad.
My datatable started looking weird whenever I tried to change the column width. This is the time I learned about updating DOM at 60 FPS and how rendering is handled by the browser. A great talk that helped me wrap my head around: What the heck is the event loop
So now I knew I shouldn’t update it on every fired event, so when do I update it? To maintain 60 FPS, you must update 60 times a second which is every 16.67 ms. I didn’t knew how to do this. I saw examples that used setTimeout
which kinda worked but it isn’t the best way if you care about timing.
Then I came across, requestAnimationFrame
which was the browsers way of handling DOM updates in an efficient way. MDN docs are great to learn about these APIs: window.requestAnimationFrame
function updateColumnWidth() {
// some code
}// not guaranteed to work every 16ms
setTimeout(updateColumnWidth, 16.67)// browser knows best when to update, ensuring 60fps
requestAnimationFrame(updateColumnWidth)
Styling tables
Did you know tables are hard to style? MDN has a whole page dedicated to it. I had to sift through many articles related to styling tables on the web to achieve what I wanted.
Also, if you are not careful about when to update styles, it may lead to layout thrashing and unnecessary reflow which is a bad experience for users. The folks at Google have explained this in good detail: Avoid Large, Complex Layouts and Layout Thrashing. The solution to this was to update styles only when required and also only at the start of a new frame. Again, requestAnimationFrame
came in handy here.
Another thing that I learned was to avoid complex CSS selectors to style elements. CSSWizardry has an excellent article on Writing Efficient CSS Selectors. TL;DR use class selectors. I replaced every querySelector
to query elements using a class
selector wherever required. I also adopted the BEM methodology to avoid nested selectors. This helped me keep styles mostly on a single level hierarchy. Querying these elements in JS also became easy, as it was just a single class.
// css classes// before
.dt .dt-cell .cell-content// after
.dt-cell__content
I had a lot of cases where I had to update the styles for a class of elements dynamically. I didn’t want to use inline styles because then I had to make sure the element was in the DOM. I avoided this problem by creating a stylesheet dynamically and then generating styles rules on the fly using the CSSStyleSheet.insertRule
API. This worked out pretty great. It looked something like this:
setStyle('.dt-cell-col-3', {
width: '80px'
})// generated styles
.dt-instance-1 .dt-cell-col-3 {
width: 80px;
}
Cleanup your events
I have known for very long that you should clean up your events. I never bothered to do it. Until it hit me.
I was using my library in another application which would display or hide the datatable based on some condition. The problem was, after the datatable was destroyed, there where events that would fire and break. This is the time I realised the importance of cleaning up.
Cleaning up events is not very hard to do. If you only have events attached inside a single element, removing that element is enough, all the events inside that container will also be removed. You only need to care about events that you might have attached outside the element, for e.g on body
or window
. You have to keep track of all the event handlers by reference, otherwise you wont be able to remove them.
Here is an example of attaching event to the body
and removing it after the instance is destroyed.
const clickHandler = (e) => {
// some code
}$.on(document.body, 'click', clickHandler);this.instance.on('onDestroy', () => {
$.off(document.body, 'click', clickHandler);
});
Browser Inconsistencies
The world wide web would be an ideal place if everybody used a single web browser. Unfortunately, there are a lot. Fortunately, we have to support only the major ones.
It is frustrating to find out, that some features that work perfectly well in some browser is behaving differently in other browsers. I also had my share of fun dealing with them. One such example is, Firefox and Edge paint a cell’s background over it’s border. It was an easy fix, but this behaviour is not obvious.
The Result
After, almost a year of learning, what we have is something I am a little proud of.
Learn more about Frappe DataTable on its homepage and GitHub. I encourage you to give it a try and leave any feedback.
I am glad that I get to build things for the web, the single most accessible platform to share your work with people around the world.