Mad, Beautiful Ideas
Adding a Column to a YUI3 DataTable

In December, I wrote instructions on how to add additional columns to a YUI2 DataTable while using YUI2in3. Since then, YUI3.3.0 was released, and in it the first Beta release of YUI3's DataTable. Recently, I decided to take some time to upgrade my implementation to use the new datatable, knowing that my needs were relatively simple and straight-forward, I figured it would be a good opportunity to test the new API and make it fit my needs.

The following implementation follows exactly my YUI2 implementation. It takes an existing HTML Table, converts it to a DataSource, and then plugs that into a new DataTable instance. YUI3, with it's plugin architecture, makes certain aspects of this easier than it was in YUI2. In spite of that though, it's clear that the YUI3 DataTable API is still pretty fresh and probably needs some work. This post is to serve both as documentation for the current state of YUI3 DataTable, as well as an examination of where potential improvements that could be made to the API.

Let's start with the module list:

        Y.use('datatable', 'datasource-xmlschema', 'datasource-local', function(Y) {
        

To begin, I'm not going to be using any of the datatable plugins, like datatable-sort, but I will address them later. First, we need to set up the DataSource. In YUI2, there was a datasource type for HTML tables, which would parse the columns out in order. Currently, no such datasource exists for YUI3, but it can be easily mapped to an XML Schema:

        var dataSource = new Y.DataSource.Local({
            source: Y.Node.getDOMNode(Y.one('#tableId tbody')),
            plugins: [
                {
                    fn: Y.Plugin.DataSourceXMLSchema,
                    cfg: {
                        schema: {
                            resultListLocator: "tr",
                            resultFields: [
                                { key: "abbr", locator: "td[1]" },
                                { key: "name", locator: "td[2]" },
                                { key: "loc", locator: "td[3]" },
                                { key: "loc_href", locator: "td[3]/a/@href" }
                            ]
                        }
                    }
                }
            ]
        });
        Y.one('#tableId').remove();
        

This will remove the table from the DOM, while still keeping it in memory for manipulation, which will be important shortly. This code is taken almost directly from the YUI3 examples for DataSource, but it also marks one of the first places I ran into a caveat with YUI3 DataSource.

In my data, the third column was optionally a link. In the YUI2 DataSource for HTML Tables, the column was read as essentially being the innerHTML of that third column, however, the way that DataSourceXMLSchema is currently implemented, it will always take the textContent of the Node before it takes the XML representation. As such, I had to grab the href attribute off the link (if it exists), which I'll be able to use with a custom formatter next, when we build the datatable.

        var table = new Y.DataTable.Base({
            columnset: [
                { key: "abbr", label: "Abbreviation", sortable: true },
                { key: "name", label: "Building Name", sortable: true },
                { key: "loc", label: "Location", sortable: true, formatter: function (obj) {
                        var data = obj.data;
                        if (data.loc_href) {
                            return Y.Lang.sub("{loc}", data);
                        } else {
                            return obj.value;
                        }
                    }
                }
            ],
            plugins: [
                { fn: Y.Plugin.DataTableDataSource, cfg: { datasource: source} }
            ]
        }).render('#datatable');
        table.datasource.load();
        

This is pretty straightforward, and most of the definition should be familiar to anyone who has used YUI2's DataTable. The only thing of note is the formatter function on the columnset definition list. It is able to take advantage of the fact that any unmatched fields in the datasource will be undefined, allowing me to use it as a condition for selectively formatting. In an implementation of DataSourceHTMLTableSchema, which I may do for the Gallery, the value will be the innerHTML, and not it's text.

So far, so good. The API is different only in it's use of plugins, and aside from the fact that DataSource clearly prefers to deal with data returned off of IO calls instead of HTML, which is a fairly minor inconvenience in my case. The work done here by Tilo Mitra and Jenny Han Donnely was, up to this point excellent. However, there are some caveats to come in the adding of the additional column.

The first step to adding the column is to add it to the datasource. This can be accessed through the table.datasource.get("datasource").get("source"). This returns the TBODY DOM Node, which is the other problem with the DataSourceXMLSchema method that I'm using. I've broken the YUI3 abstraction by being provided with a raw DOM Node, something which doesn't seem correct with the rest of the library. Again, something a DataSchema designed for HTML Tables will be able to handle more appropriately.

        var schema = table.datasource.get('datasource').schema.get('schema');
        schema.resultFields.push({ key: "new", locator: "td[4]" });
        table.datasource.get('datasource').schema.set('schema', schema);
        
        Y.Array.each(table.datasource.get('datasource').get('source').rows, function (row) {
            var node = new Y.Node(row);
            node.append("New Column!");
        });
        

Aside from the brief cognitave dissonance of getting a raw DOM Node, and the verbosity of getting properties off of nested plugins, this is not a difficult process, but actually adding that column to the DataTable is where things get more difficult. In YUI2, it was as simple as calling table.insertColumn() with a new column definition. In YUI3, doing this required me reading a lot of the internals of DataTable, which was not completely pleasant. But let's start with the code.

        var columnset = table.get('columnset'), columns = columnset.get('definitions');
        columns.push({ key: "new", label: "This is a new column", sortable: true});
        columnset.set('definitions', columns);
        columnset.initializer();
        table.set('columnset', columnset);
        table.datasource.load();
        

Looking at it now, it's a bit anti-climactic. It isn't that much code, but writing it took a deep understanding. But I'm going to hilight a few of the most important calls. The value pushed onto the columns array is the same as the column definition when building the datatable, however, what is actually done internally is YUI builds up a ColumnSet and a collection of Y.Columns. It would, no doubt, be possible to create the Y.Column instance directly and append it to the ColumnSet, but by modifying the definitions and reinitializing the set, I don't need to know the details of how it's set up. Finally, setting the columnset Attribute on the table, forces the table to generate the table headers for the new columns, and finally we need to reload the data.

The above code came from a desire to do as little work as possible in the backend, as adding a new column seems that it should be fairly easy. However, in truth, while I'm not creating a new ColumnSet object, I'm doing all the work except creating a new object. I could just as easily take the definitions array, and pass that into table.set("columnset", columns);. This does a bit more work, but probably not much, and saves a couple of lines of code in my source, looking perhaps a bit simpler.

        var columns = table.get('columnset').get('definitions');
        columns.push({ key: "new", label: "This is a new column", sortable: true});
        table.set('columnset', columns);
        table.datasource.load();
        

Either way, this is not an ideal API. To start, an 'addColumn' or 'insertColumn' method is required, either on the DataTable, or the ColumnSet. Putting it on the ColumnSet makes a lot of sense, but it would still require the set("columnset" method on the DataTable in order to ensure that the DataTable was updated with the new column definitions, or the columnset should fire a changed event that the DataTable can respond to.

There is, however, one potentially big issue with DataTable in it's current implementation. When table.datasource.load() is called, it creates a new RecordSet object which is what is the actual underlying data for the DataTable. This is, in part, because DataSource is mostly stateless. It's designed to be pointed at a collection of data, and to return that collection to a callback function. RecordSet actually contains that data. The problem with the current implementation of the DataSource plugin for DataTable, is that it completely replaces the RecordSet in use, which can actually break other plugins on DataTable.

For instance, I was using the DataTableSort plugin to allow my headers to be clickable to sort by any column. This plugin works by augmenting the DataTable's recordset with the RecordsetSort plugin to help with it's implementation. However, since DataSource replaces the Recordset, instead of modifying it's data (even if that were to be by emptying it and reloading it), every call to table.datasource.load() must be followed by replugging the RecordsetSort into the Recordset, which is not expected behaviour. I also found DataTableSort to be painfully slow, but that might be an implementation detail on my end, I haven't determined yet.

YUI3 DataTable doesn't support editing yet. Row Clicks are not explicitly supported (though easily delegated). In short, if you just need to display data and want to allow some interaction, such as sorting and filtering, then YUI3 DataTable will probably meet your needs, but it has a ways to go, and the API is liable to change going forward. Still, it's a solid core to work from, and I look forward to seeing it moving forward, especially if these API issues I ran across can be fixed.