A Vue data-table for rails

Torsten Ruger
rubydesign_web
Published in
6 min readJan 25, 2018

In this post we’ll be writing a Vue data-table component that can be sorted, searched and configured. In 50 lines of code! It assumes you know the rails setup I’ll be working in, demonstrated in my last post. Here is the table demonstrated:

Data table in vue

Simple template

Let’s start with a simple table template. We’d want to our users to pass in the column names, so we can iterate over them, and then we iterate over the column data. Forgetting about searching and cell config, we could use:

%script{'type'=>"text/x-template", id: 'table-component'}
%table.table.table-striped.table-sm
%thead
%th{ "v-for" => "id in column_order" ,
":class" => "{ active: sortKey == id }"}
{{columns[id]['name']}}
%span.arrow{":class": "sortOrders[id] > 0 ? 'asc' : 'dsc'" ,
"@click": "sortBy(id)",
"v-if": "columns[id]['sortable'] != false"}
%tbody
%tr{"v-for": "row in filteredData"}
%td{ "v-for": "id in column_order"}
{{ row[id] }}

We’re using bootstrap classes for basic styling and calling our component “table-component”. We can see some of the data that is passed in, ie the column_order we iterate over twice, and also the columns we use. “filteredData” on the other hand is a computed property, derived from the table_data that gets passed in. The css for the arrows is not show, but quite easy, and we can see the v-if in the header, which makes sorting optional for columns.

Simple component

Let’s have a first shot at the component code, that would work with the template:

class TableComponent < Vue
template "#table-component"
props [:columns, :table_data ,:column_order]
def initialize
@sortOrders , @queries , @sortKey = {} , {} , ""
self.column_order.each do |c|
@sortOrders[c] = -1
@queries[c] = ""
end
end
def sortBy(key)
return if this.columns[key][:sortable] == false
@sortOrders[key] = @sortOrders[key] * -1;
@sortKey = key;
end
def filteredData
dat = this.table_data
return dat unless(@sortKey)
order = @sortOrders[@sortKey] || 1
type = self.columns[@sortKey]['type']
return dat.slice().sort do |a, b|
aa = a[@sortKey]
bb = b[@sortKey]
return (aa === bb ? 0 : (aa > bb ? 1 : -1) ) * order
end
end
end

This is actually quite close to what the grid-example on vue site gives us already. Apart from above using ruby2js obviously: this means that instance variables are data, and props are defined by that class function.

We set the sort key from the click handler. And then we sort the data according to it in the filteredData. Quite straight forward really. Vue takes care of redrawing the html if the filteredData changes. “Reactively” (i’d say magically).

With that we could start, but let’s add all the features that were promised first.

Searching

To add searching we look for columns that have “searchable” set in their column data. Then we need to add an input field to the header, after the span for the arrows, like this:

%input{:category => “query”, “v-model” => “queries[id]” , “v-if”: “columns[id].searchable”}

The code above already set the “queries” as a hash and initialized it (remember in Vue only existing variables are reactive). And the rest is just more code in the filterData method, roughly like:

this.column_order.each do |col|
query = @queries[col].toLowerCase()
next unless @queries[col].length > 0
dat = dat.filter do |item|
return item[col].toLowerCase().indexOf(query) > -1
end
end

Which just goes through all input fields that have value and filters the data on the appropriate column.

Customization

Now the only thing missing is customized cell rendering. The current code just dumps the appropriate value, but sometimes we might want a link, or a button, or a tooltip. Some Vue tables out there solve this problem by letting you pass in a render function. And this works off course, but it does have scope issues and means you have to write code that generates html which imho is best avoided.

A much simpler solution can be achieved by slots. Slots allow what Vue calls content distribution, basically just what we want. Namely for the user of a component to squeeze content into the component from the outside. Rails users will be familiar with “content_for :name” and “yield :name” which achieves the same thing.

Slots may be named, and as it turns out even dynamically (through code), so we can define a slot per cell, like this:

%td{ “v-for”: “id in column_order”}
%slot{ “:name”: “id” , “slot-scope”: “row” , “:object”: “row”}
{{ row[id] }}

This is exactly the same for loop as before, and also the same content (the row[id]). But the slot line make is so, that if a slot with the given name is defined in the parent scope, it will be used. And, even better, it will get the current row, passed as an “object” variable, so it can use all the attributes on the row to create output. btw, “row” is the scope used for this variable passing, so to access an attribute, the parent would write “row.object.attribute”.

Usage

Now that we have a component, let’s look at an example of how to use it. A product list, but i’ll remove some attributes for brevity.

:javascript
class App < Vue
el '.purchase-app'
def initialize
@products = #{ render( partial: "purchases/products.json") }
@product_order = [ :scode , :name , :category , :add]
@product_columns = {scode: {name: "#{t(:scode)}" },
name: {name: "#{t(:name)}" ,
searchable: true},
category: {name: "#{t(:category)}" },
add: {name: ""} }
end
def to_basket(product)
# code ommitted, basically adding product to purchase order
end
end

As mentioned in the previous post, we take advantage of the fact that rails parses this, and we can thus pass the translation string and product data easily. The products partial can be built with jbuilder or rabl. Also notice the “add” column, which we’ll come back to in the template part:

%table-component{ ":columns": "product_columns" ,
":table_data": "products",
":column_order": "product_order"}
%template{ slot: "name" , "slot-scope" => "row"}
%a{ ":href": "'/products/' + row.object.id"}
{{row.object.name}}
%template{ slot: "add" , "slot-scope" => "row"}
%a{ "@click" => "to_basket(row.object)" }
=image_tag "plus.png" , width: "30px"

A “plain” table would only have needed the table-component line to pass the data in. But here we see two slots, overriding the way those columns are rendered. The name column does a simple link to the product, while the omnious add column creates an action link for the product.

There are a few more details to note. The template tag is a Vue general purpose wrapper, that doesn’t get rendered. This is useful as we can’t just write a td at that place: that would create invalid html and the browser would ignore it. So we add the slot : “name”. Also notice how the link receives the row, called object, in the row scope. “row.object.name” thus resolves to the product name. Us having differently named slots is due to the fact that we used “:name”, not “name” for the slot definition above.

And finally we use the benefit of the rails asset pipeline, by using the image_tag helper, even inside the vue template. Off course if we would have wanted the product picture we would have had to pass that into the json data, as we can only call the helper on the server, not the client (ie on static data).

Conclusion

That concludes the component. Off course in real life what happened is that i wrote an app with this functionality. Then i started another, which started to look more and more the same. And then i extracted the code into a component. Which is the way things should go. If you start by thinking that you will need a component, you are already on the wrong track. Start by writing the app and extract later.

What we have now is:

  • a Vue component defined inside a single haml file
  • sortable columns (optional)
  • searchable columns (optional)
  • easy cell render overrides ( no code needed)

All in 50 lines of code!!

Later I added a 51:st: a slot for a possibly trailing table row. Useful for totals or the like.

I hope you enjoyed this, and possibly learned something. Next up is are data charts with chartist, which at 300 lines i can unfortunately only outline.

--

--