08
Apr
2016

Making VueJS and DataTables Play Nice

Vue / DataTablesI recently started using VueJS.  Well, I suppose more specifically I've recently started learning VueJS by using it on a project.

The project was formerly a rather messy nodeJS/AngularJS project which is being rebuilt with Laravel, but parts of the frontend needed to remain more real-timey.

Anyway, I needed Datatables to play nice with Vue so that I could leverage it to render dynamic tables with fixed headers and columns (the whole purpose of picking DataTables in the first place).

When I built this in Angular I was able to make use of a custom angular directive that I found, it wasn't ideal but it worked -- mostly.  I was unable to track down anything similar for Vue, so I built something myself.

I first did the most naive thing possible and tried to render a table with Vue directly, and then attempt to target that with DataTables.  I was able to get it to work intermittently, but I could not get it to work properly if I tried to dynamically change the data.

Sure, I could have probably just leveraged the ajax ability of DataTables directly, but I am also using my dataset elsewhere on the page and it didn't make sense to me to hit the server for the same data multiple times, so that approach was out before it was even an option.

The solution, of course, is to pass the data to DataTables via javascript.  Obviously... we're working in Javascript anyway, so that totally makes sense.  The difficulty though comes in when you want to dynamically update the data in the table.

As it turns out it's really not that difficult, but figuring out how to do it is a bit challenging.  DataTables has so many options, and it's documentation is very dense and not the easiest to read so figuring it out is a lot of trial and error, at least in my experience.

 

Ok.. lets get to it.  So I have a computed attribute in my top level Vue component that takes whatever my dataset it and applies a bunch of filters to it (as set in other child components).  One of the components, for display of this data, displays it in a block view, with a nice little pager and so on.  It's all internal company financials data, so unfortunately I can't really share screenshots.  The other display is a table view, and because the number of fields necessary to be included in the table is rather large, I needed to be able to fix the header and left column so that the table can be scrolled and you still know what item, and column you're looking at.  Enter DataTables.

So I have a table component that looks something like this:

<template>
  <table id="table-id"><table>
</template>

<script>
  export default {
    props: ['tableData'],
    data() {
      return {
        rows: [],
        headers: [
          { title: 'column 1' },
          { title: 'column 2' },
          { title: 'column 3', class: 'some-class'}
        ],
        dtHandle: null
      }
    },
    watch: {
      tableData(val, oldVal) {
        let vm = this;
        vm.rows = [];
        val.forEach(function (item) {
          let row = [];
          row.push(item.col_one_data);
          row.push(item.col_two_data);
          row.push(item.col_three_data);

          vm.rows.push(row);
        });

        // This is where the "magic" happens...
        vm.dtHandle.clear();
        vm.dtHandle.rows.add(vm.rows);
        vm.dtHandle.draw();
      }
    },
    ready() {
      // Determine the height of the table based on where it is on the page, 
      // or force a minimum of 300px if it's too close to the bottom of the window.
      let scrollY = (Math.floor(
        window.innerHeight - this.$el.getBoundingClientRect().top
      ) - 140);
      if (scrollY < 300) {
        scrollY = 300;
      }
      // Fire up datatables with our desired config
      // and store a reference handle to our component's
      // data element so we can reference it later..
      this.dtHandle = $(this.$el).DataTable({
        columns: this.headers,
        data: this.rows,
        data: this.rows,
        searching: false,
        paging: false,
        fixedHeader: true,
        fixedColumns: true,
        scrollY: scrollY + 'px',
        scrollX: true,
        info: false,
        buttons: [
          {
            extend: 'colvis',
            collectionLayout: 'fixed two-column',
            text: 'Show/Hide Data'
          },
          {
            extend: 'csv',
            text: 'Export CSV'
          },
          {
            extend: 'excel',
            text: 'Export Excel'
          }
        ]
      });

      // Prepend the buttons to the wrapper so they appear above the table:
      this.dtHandle.buttons()
          .container()
          .appendTo('#table-id_wrapper');
    }
  }
</script>

So, when I want to place my datatable on the page, I just call my custom component and pass my filtered data to it like so:

<my-table :table-data="filteredData"></my-table>

filteredData is a computed value on my parent component, so that any time I change any of my filters that alters the dataset, the filtered data gets passed into the table component.

 

So.. how and why does it work?

Well... DataTables can only be instantiated with config one time per element.. if you want to change the data that's in the table you have to clear the existing data, add new rows, and then redraw (see 'where the magic happens' in the code above).

I have a watch on the filtered data that's passed in to the table component, so every time the data changes, it loops through the new dataset, builds up the new rows array, then clears the old table data, pushes in the new data, and redraws the table. The end result is I have a totally dynamic table, whose contents is determined by a handful of filters elsewhere on the page, that has fixed header, column, and some handy buttons to toggle column visibility and provide some export options for good measure.

It's really pretty slick.  When the component is ready it attaches the Datatable to the table tag, then when data becomes available and/or changes, it updates the datatable, by referring to it with the handle we setup initially in the ready function.

 

Why Vue, why not stick with Angular?
Fair question I suppose.  I don't really have a good answer.  The existing project is something I wrote in nodeJS with and Angular front end.  It's currently being re-written in PHP, as I don't really have many coworkers very well versed in node, and the project has become an unmaintainable mess.  So a rewrite in PHP, which most of my fellow devs are totally competent in made sense, and since it was being rewritten anyway, why not try a new front end library too?

I'm pretty happy with Vue thus far.  It doesn't try to do as much as Angular does, which is just fine with me since part of this rewrite is moving the lions share of heavy data manipulation to the server side, so the frontend mostly just needs to present the data, not do a bunch of manipulation of it too, and Vue has been very easy to get up to speed with, and seems to be very performant too.

Hopefully this info will be useful to someone else.. I fought with it for far too many hours over the past couple days, so my hope is this post can help prevent some of that pain for others.

Have you tried VueJS? What sort of cool things are you building?

UPDATE: I've come to realize that the code above is not sufficient for people to understand this. As such, I've created a working demo on codepen: http://codepen.io/willvincent/pen/LbeKKW