smarteee

Wooohoo! a responsive slickgrid with dynamic row-heights

TODO: bung this all in a git repo etal... meanwhile click here for the craft...

have all the fun with it ;)


Filter Min
Filter Max

  • ☠
  • ☰

proof of concept



Imo the Slickgrid jQuery plugin has near ubiquitous application, given that so many sites involve presenting data, &nicely,, But because the model is thin (which is a good thing) some folk seem to go in search of other options when it does not meet their exact requirements.

The Slickgrid API provides all that we need in order to create dynamically sizing row-heights and all without any need to hack on the Slickgrid codebase. Because:

  • we can add/remove rows dynamically;
  • we can dynamically apply custom css at row & cell level.
If you suffer from tldRitis or happen to think that code is the documentation then you might as well just go straight here. Otherwise or out of curiosity, do peruse the full low-down on the voodoos..

First, a simple demo to get things started. Each time a row is expanded, a random amount of detail gets generated. Correspondingly, each time the row is collapsed, the detail is removed:

Incidentally, this is the kind of approach that the also excellent DataTables provides pretty much natively through its API. &imo is quite neat. But what i'm presenting here is (to borrow a term) a kind of Slickgrid pollyfill and works by:
  • Adding a bunch of dummy padding rows;
  • Providing a static column formatter callback: onRenderIDCell and using this to overlay the padding with a detail panel.
Here's the onRowClick event handler with the codes for adding and removing the padding rows highlighted:
so far so good ..buuut, this implementation does happen to involve a slight hack in order to inject our detail panel into the slick-row.. here's all the codes with the relevant sections highlighted:
& here's the relevant css:
Well in any case that's less than 200 lines of customisation codes so.. w00t
fiddle with it!
Now. To do dynamic row-heights per cell, we employ similar trickery but we must also deal with a few side-effects along the way. I have broken this down into the following sections:
  • styling away the row divisions
  • moving and resizing columns
  • sorting columns
  • filtering rows
  • post rendering
  • further optimisations
  • front-page example
And you should of course be interested in reading these:
  • caveats
  • biG Fanks

  • ☠
  • ☰
  • ⌘

styling away the row divisions



we must:
  • remove row borders
  • remove unwanted row stripeyness
  • escape the per cell overflow clipping
Here is what it looks like. Each time a row is expanded, a random amount of detail gets generated only in a specific cell. Correspondingly, each time the row is collapsed, the detail and the padding rows are removed.

To style away the unwanted row artifacts, the Slickgrid API provides row-based styling via the Grid.getItemMetadata callback interface. But because i'm making use of the DataView, the DataView.getItemMetadata default implementation (which is to do nothing) must be overridden.
_dataView.getItemMetadata=onRenderRow; //override the dataview callback with our own

Here is the callback implemenation:

And here is the css:
Notice on line 152 in the onRenderRow callback that i am providing another callback: onRenderDetailCell. This is like the onRenderIDCell column formatter but works per cell & can be provided in the run-time. We at least need to use this callback in order to dynamically set the new height of the detail cell. But i am also wrapping the detail in a custom container to provide a further styling opportunity for our cell content.
Notice also on lines 148-150, we invoke the Slickgrid Grid.setCellCssStyles API to add a dynamic-cell class that sets the overflow to visible in order to style away the per cell overflow clipping. I'm doing this stuff in here instead of say, the onRowClick handler because Slickgrid maps the css to a fixed row number. Which is a bit strange. It means that if rows are added or removed the style slips away from the row it was originally applied to.

We only want the css applied to expanded parent rows; if the row is padding or a collapsed parent, it needs to be removed. Therefore on lines 127 & 156 we invoke the Grid.removeCellCssStyles API to get rid of the unwanted dynamic-cell class, if it happens to exist for that row.

NB: beware! attempting calls to addCellCssStyles, removeCellCssStyles (or getCellNode) when a row does not exist (ie: it's disappeared out of the viewport) can cause Slickgrid to bork :(

Here is the entire code module with the relevant sections highlighted:
That's still only 250 lines of customisation and better yet, zero underhanded hax ;)
fiddle with it!

  • ☠
  • ☰
  • ⌘

moving and resizing columns



In the preceding examples it is in fact already possible to move and resize the columns. But we are using some non-responsive detail content. In this next example the detail content responds to a change in cell-width. Padding rows are dynamically added and removed accordingly.

Moving columns is easy. all we need to do is ensure never to directly reference column indices:
              var getIdxDetailCol=function() {return _grid.getColumnIndex(_attribs.exampleDetailField);}
            

Responsive resizing requires some work.
We need to re-factor the codes by creating a couple of functions for adding and removing padding rows.

Also, we've abstracted the height calculation out of the kookupDynamicContent content generating function.

And now we can implement a handler for the Slickgrid Grid.onColumnsResized event:

Here is the entire code module with the relevant sections highlighted:
Juuust sneaking in there at under 400 lines..
fiddle with it!

  • ☠
  • ☰
  • ⌘

sorting columns



Some work to do here. but not all that much. we must ensure that no matter whether we are sorting up or down, the padding remains contiguously underneath the parent.

Here's the little monster all wrapped up in a closure:

And that's about all there is to sorting.

All that remains to do is subscribe to the Slickgrid Grid.onSort event and provide a handler:
              //////////////////////////////////////////////////////////////
              //delegate the sorting to DataView; which will sort the data using our
              //comparer callback; fire appropriate change events and update the grid
              //////////////////////////////////////////////////////////////
              var sort=function() { _dataView.sort(comparer.compare, comparer.getSortAsc()); }

              //////////////////////////////////////////////////////////////
              //fired when the user clicks on a column header
              //////////////////////////////////////////////////////////////
              var onRowSort=function(e,args) {comparer.setInfos(args.sortCol, args.sortAsc); sort();}
            

I also provide a wrapper for the call into the DataView.sort. In addition to being called from our onRowSort handler, this can also be invoked anytime we feel like it; on initialisation, on a data update; whenever.


Here is the entire code module with the relevant sections highlighted:
fiddle with it!

  • ☠
  • ☰
  • ⌘

filtering rows



Well that's resizing and sorting in the bag. And happily, there's not much to filtering beyond a typical amount of customisation that might ordinarily be expected.

Filter Min
Filter Max
Here's the enclosured filter. Most of the code is to do with the UI and might be required in any case. The actual filter is a one-liner; if the row is padding we just get the value we're filtering on from it's parent. Really simple.
Here's the implementation of the onFilter callback we provide to the DataView.setFilter API:
              var onFilter=function(item) { return pcFilter.onFilter(item); }
            
Here's all the codes:
Here's all the css:
And for completeness sake, here's the html:
Having removed the test-data-kooker ~which is only fair~ that's all the basics: resizing, sorting & filtering in under 500 lines of fairly legible, liberally commented custom javascripts.. acu
fiddle with it!

Now. As can be seen in the last three examples, i'm cooking up some funky on-the-fly svg. which is xml & not very jQuery friendly. & it's a bit of an overhead to get the html string for the Slickgrid cell render.

And it's slightly vexing to keep adding & removing the dynamic-cell css class all over the place.

All of which, i'm going to use as justification for something that not only makes life easier; but also provides an option for some optimisation..


  • ☠
  • ☰
  • ⌘

post rendering



All the row and cell formatter callbacks that i've used so far get invoked prior to Slickgrid rendering the row. it would be nice if we could do stuff on a post-render event. The Slick.Grid. API does provide an asyncPostRender column option where we can supply an event handler. But the event fires on a setTimeout (; perhaps @Tin was trying to cut-down on the event-hell ;) and even with a timeout of zero it is wayyy laggy for our purposes.

Never mind. There are alternatives:


option #1 MutationObservers

With a caniuseit rating of 83% (figure will rot for the better) we can instead use a MutationObserver to generate custom post-render events:


Filter Min
Filter Max
Notice the difference?
~Well hopefully not. But here's what changed in the codes. Firstly the MutationObserver:

We set it up to watch the grid canvas and fire events when elements with a slick-row css class get added:
              internationalObserver(_grid.getCanvasNode(), _attribs.slickRow, onAddRow);
            

Then in our onRenderDetailCell formatter callback, instead of supplying the detail as a raw html string like before; we now add a container element with an id that identifies the item/row and the column and we give it a css class name of dynamic-cell-detail-ctr which we will use merely as a selector.

Here's the onAddRow event handler that gets fired by the MutationObserver. This in turn fires our onPostRender handler for any cells in the row that have been marked with the dynamic-cell-detail-ctr class:
              var onAddRow=function(row){$(_attribs.classSelector("cellDetailCtr"), row).each(onPostRender)}
            

And here's the post-render event. This is now where we inject our detail into the DOM and apply the dynamic-cell css class in order to escape the overflow-clipping on the slick-cell container:

Here's all the codes with the relevant sections highlighted:
Hang on tho we've just exceeded 500 lines of codes.. ooops
fiddle with it!

Hmm, so apparently we have in fact added more new code than we have removed. -What then exactly have we gained from the dozen or so extra codes ?
Well. Now we have a lot more control over our dynamic detail content. Here's some things we can do:
  • pre-build our content anytime we like; either on-the-fly or from a template;
  • easily calculate the contents' height requirements by putting it into some invisible space within the DOM;
  • manage our own content cache when slick rows go out of scope
And it is this last point that will be the subject of the next section.. But before we get to that i will briefly discuss another option:

option #2 onLoad

Something that also seems to work as an alternative means of generating onPostRender events is a bit of a hack involving inserting dummy <style> elements with an onload attribute.

Here's the changes in the onRenderDetailCell callback:

The onPostRender function is almost the same as before only now the item.id is provided directly as the first function argument:

This approach may have more cross-browser compatibility than MuationObservers & seems to work ok in the latest versions of Firefox, Safari and Chrome. But on Chrome there is a touch more latency. And in an older version of Chrome i tried (Version 20.0.1150.1/Linux) it seemed to be generating garbage that the gc was unable to retrieve.?. i'm not sure why. there are no circularities afaik.. :/

fiddle with it!

  • ☠
  • ☰
  • ⌘

further optimisations


Right-ho, so now we have millions of rows of data + a shed-load of padding. filtering is O(n) so that's not too bad. but the standard sort implementation in the DataView is a bubble-sort; the worst-case complexity of an unsorted list is O(n2).. It would be nice *not to have to wade thro all the out-of-scope padding.

Well obviously, having expandable/collapsible rows helps to mitigate the issue. But suppose we don't want that feature? -in that case, we can implement our own caching system. The following example is based on the previous example but reaps out-of-scope padding beyond a certain threshold. And of course it revives the padding as the expanded detail rows come back into scope.

I have chucked in a logger to highlight the process. (; And for this reason the example still features expando-rows; so there's not a total flood of messages ;)


Filter Min
Filter Max
First, we extend the MutationObserver so that it additionally fires removed-row events:
Then in the onDetailRemoved event-handler, we add the row-item to an internal list. All padding rows within this list are going to remain cached:
Next we implement an idle-events pump (; which we can also re-use in the filter ;) to generate a bunch of onSysIdle events whenever the application is not up to much.
Now we can implement functions to reap and revive padding rows that drift in & out of the cache:

In this example, the revivePadding function gets invoked from within the onPostRender event handler, once the row moves back onto the Slickgrid canvas.

And the reapPadding function gets invoked by the onSysIdle event handler [NB: this could be improved by only kicking-off idle-events in response to say the grid being scrolled].

Here's all the codes with the relevant sections highlighted:
Incidentally, forget trying to use the jQuery special event stuff such as the remove event. Mr.Leibman uses native DOM methods to create and destroy Slickgrid elements so that will not work here.

fiddle with it!


  • ☠
  • ☰
  • ⌘

front-page example


Finally, a brief explanation of what's going on in the example on the Landing page. Since there are not 100Ks of records, it's not employing any caching but otherwise supports all the functionality discussed so far.

The big (&hopefully) noticeable differences are that i have done away with the expandable rows feature and i have made it so that there are two fields that contain dynamic cell detail content.

So this example really is a kind of place-holder for what needs to be done next in order to make a generic Dataview-like Slickgrid extension-mod.

Here's the codes with some notable sections highlighted:
See on lines 417-511, i have replaced our old kookupDynamicContent function with a couple of new functions: setCallsContent and setStatusContent, which are specific to each dynamic field. These should be part of the application and *not inside any extension-mod.

See also lines 557-615, where i have provided an API to add/update/remove data items. So likely, it is in here that an extension-mod should instead invoke callback-handlers provided by the application for the above custom setters.

fiddle with it!

  • ☠
  • ☰
  • ⌘

caveats


I have only covered the basics. There is that whole selectable/editable aspect to Slickgrid that i have not yet scratched. In my defence, i am part-way thro my own implementation of all this stuff & this post already goes beyond the scope of my current project.

Also:

  • example-code only; not production ready. you have been warned etc etc =)
  • examples seem to work in most modern browsers; i have /not/ tried with iE; versions >= 11 might be ok..
  • since i've crammed everything onto one ginormous page, the grids here are somewhat sluggish. use the jsfiddles if this is of concern.


  • ☠
  • ☰
  • ⌘

biG fanks



..For providing the pixie-dust..

Special thanks go out to:

  • var SyntaxHighlighter = { excellent: true }
  • the very splendid white rabbit font