Using Nested YUI 2 DataTables for Row Expansion

By SatyamMarch 17th, 2010

Daniel Barreiro (Satyam)About the Author: Daniel Barreiro (screen name Satyam) has been around for quite some time. The ENIAC was turned off the day before he was born, so he missed that but he hasn’t missed much since. He’s had a chance to punch cards, program 6502 chips (remember the Apple II?), own a TRS-80 and see some fantastic pieces of operating equipment in his native Argentina which might have been in museums elsewhere. When globalization opened the doors to the world, his then barely usable English (plus an Electrical Engineering degree) put him on the career path which ended in a 5-year job in the Bay Area back in the days of NCSA Mosaic. Totally intrigued by the funny squiggles a friend of his wrote in his plain text editor, full of <’s and >’s, he ended up learning quite a lot about the world of frontend engineering. It’s been a long journey since COBOL and Fortran. Now he lives quite happily semi-retired in the Mediterranean coast close to Barcelona, Spain. When he’s not basking in the Mediterranean sun, Satyam can be found among the most prolific and knowledgable participants in the YUI community on the YUILibrary.com developer forum.

As usual, it is developers on the YUI forums who come up with the most interesting questions (tip: this makes the forums a good place to hang around). Recently, someone asked the following: Using YUI 2 DataTable, could you nest a child table to provide details about a row when it is “expanded” in a master table? It has been asked a few times before, but I haven’t had a good solution to share in the past. Now I do have a solution, and you can find it, along with my other examples, here.

This is what it looks like:

The top input box is actually a YUI 2 AutoComplete box where you can first look for a particular music artist. When you find in the dropdown list the artist you are looking for, selecting it will bring up a DataTable listing all the albums for that artist, ordered with the most recent albums at the top. The [+] sign to the left of each row allows for that row to expand; when the row expands, a nested DataTable is displayed listing the tracks in the selected album.

The nested child table is indented to the right, leaving the column with the expand/collapse icon encompassing it. Several child tables can be open at the same time. The master table can be sorted and the child tables will move along with their master records.

The technique we’re using here involves changing the height of the row in the master table so that it leaves enough space for the child table to overlap it. The code in the sample is heavily commented, so here I’ll just describe the logic. First, the child table is created and appended to the document.body and removed from the pageflow (position:absolute). The width is set to the width of the master table minus the width of the expand/collapse column. Only then is the height of the child table measured, since narrowing the child table can cause the text on a cell to wrap (like in the second track), increasing the height. The height of the master row is increased by the height of the child table. In fact, it is the height of the cell containing the toggle icon the one that gets adjusted, the row will simply match the tallest cell. The position of the child is then set to the position of the master row, offset to the right to clear the expand/collapse column and down to clear the master row.

It is important to keep track of all the information to do this. DataTable records are a good place to do so. A record object can take extra information beyond what was originally read by the DataSource. If you use method setData() on a new field, that field will be created if it didn’t exist before. We store all related information in the field associated with the expand/collapse column, which is called __NESTED__ and holds an object that has the following properties:

  • td: a reference to the expand/collapse cell in the master table
  • tdOrigHeight: the original height of that cell, used as an offset for the child table
  • tdNewHeight: the height with the child table, used when expanding a second time
  • dt: a reference to the child DataTable instance
  • div: a reference to the container for the child DataTable
  • expanded: whether the row is expanded or not

The existence of a value (not undefined) for this field tells us that the child table exists, whether visible (expanded:true) or not.

Positioning is done in two steps. When the table is created, the horizontal position (left attribute) is set just once. The vertical position (top) is set in a second step along with those of other records. While the left position is stable, expanding and collapsing rows or sorting the master table makes the rows move up or down (but not horizontally); when this happens, the vertical position of all child tables needs to be moved accordingly. (Note: From a positioning perspective, it might have been easier to make the child table part of the parent table and use position:relative to let the browser move it for us. Though it makes the positioning easier, this approach creates other potential issues. Since the child table would become part of the same branch of the DOM tree as the master, styles would propagate down from the master table to the child, events from the child table would bubble up to the master, and so on.)

In this example, you can keep querying for different artists, which means a new master table and new child tables. It’s important not to forget about those child tables and leave them behind. When a new artist is requested, we make sure to destroy all the child tables and their containers by first going through the RecordSet and, for those Record instances that have a __NESTED__ field we call the destroy() on the child tables and then remove the whole child from the DOM tree.

YQLDataSource: Getting Data from YQL

All the data both from the AutoComplete and for the several DataTables is read via YQLDataSource, a subclass of ScriptNodeDataSource that uses the YUI 2 Get Utility to fetch data directly from the YQL Service. You usually don’t need to provide any arguments when creating an instance of a YQLDataSource. It already points to the URL for the YQL Service so you don’t want to change that. YQLDataSource will read all the fields that it receives from the servers. On the one hand, this means you don’t need to provide a responseSchema.fields list of fields, but on the other it means that you shouldn’t use Select * in your YQL query; rather, list the specific fields you want to retrieve in the YQL statement. You may still use the responseSchema.fields array to attach parsers for some of the fields if they are numbers (as many fields in this example are), dates, Booleans or come in special formats.

Since YQLDataSource is a subclass of ScriptNodeDataSource, it can be used with any YUI component that uses a DataSource. I used a YQLDataSource for the AutoComplete box, another for the main table and one shared YQLDataSource for all child tables. Since the format of the reply for all child tables is the same, there is no problem reusing that single instance of YQLDataSource amongst them. If there had been anything worth plotting, I might have also used Charts with YQLDataSource.

YQLDataSource takes the YQL statement as the first argument in its sendRequest() method. That means that in a DataTable, it is the value you set in the initialRequest configuration attribute or you pass to my requery() method, which is also included in the page. For AutoComplete, you assemble the YQL statement in the generateRequest() method that you must override. All YQL statements used in this example are stored in three YQL_QUERY_xxxx constants near the top of the code. YAHOO.lang.substitute is used to assemble the query with its arguments.

The expand/collapse column is initially empty; it has no data coming from the server. The column is added on the spot and then the associated data field is used to store the settings for the nested table. The formatter associated with it adds an invisible <a> element so it can serve as a tab stop and can hold a suitable ARIA role and status. It has a className that sets the [+] sign as a non-repeating background, like this:

.yui-skin-sam .yui-dt td.__NESTED__ div.expand {
    background:transparent url(http://yui.yahooapis.com/2.8.0r4/build/assets/skins/sam/sprite.png) no-repeat 0 -350px;
}

There is a similar style declaration for the collapse icon. This makes it really easy for the visual designer to completely change the look of the page if needed. If I had set the contents through the formatter for that column setting its contents as text, an image or a button, there would be no way to change it without changing the code. In this way, the cell content remains invisible and the styling is fully in the hands of the designer.

To toggle the nested tables we respond to any click on that cel. To handle clicks, we can simply rely on DataTable’s cellClickEvent:

albumDt.on('cellClickEvent', function (oArgs) {
    var target = oArgs.target, event = oArgs.event,
        record = this.getRecord(target),
        column = this.getColumn(target);
   
    // We care about clicks on columns 'expand' and 'title'                   
    switch (column.key) {
    case 'expand':
        Event.stopEvent(event);
        // . . . . 

First I find out, from the event target (which is the <td> element), the record and column corresponding to that cell. From the key of the column I then decide what to do and from the record I get all the information I may need.

Final Thoughts

This is an example, and it has some rough edges. If you resize the browser window, the child tables may end up floating in weird places. Further event listeners would be needed to detect such changes and redo the layout. Another enhancement would be to leverage ARIA live regions to make the child tables more discoverable to screen-reader users; in its current form, this example would fare poorly in a screen reader because of the dissociation between child tables and their corresponding rows in the master table.

YQL is a query system for external tables or data APIs and it cannot do any better than the tables or APIs it represents. The search for artists only works on full names, it won’t find an artist by partial names, which makes the AutoComplete search box behave a little funny. Still, for the purpose of the example, it is the best table I could find because it has a three level hierarchy: artist – album – tracks.

A more complete version of this example is also available with a general-purpose YAHOO.widget.NestedDataTable object defined as a subclass of DataTable in a separate .js file, where several of the shortcomings of the original are fixed.

11 Comments

  1. Have you seen the Expandable Rows example for YUI DataTable? It uses the same principle and might already have solved the rough edges you mention.

  2. John
    I have and it really doesn’t work that good. For one, DataTable uses the sectionRowIndex property of TR elements for a lot of things, my editor counts 17 occurrences of it in datatable.js. All of those come out wrong with extra rows added as that example uses. You cannot insert rows into DataTable and expect it to fully work. As soon as you add very little functionality, the whole thing breaks down.

    Besides, as I mention in the text, nesting one datatable within another brings up some problems such as styles propagating down from master table to child table or events bubbling up from child to master. The nested datatables cannot be in the same branch of the DOM as the master table, thus, they have to be separate and just positioned to look right, but not actually related hierarchically as far as the DOM is concerned.

    Without explicitly stating all those issues, that example should not even be there.

    Also, please see: http://yuilibrary.com/projects/yui2/ticket/2528595

  3. I wholeheartedly disagree with your statement that the example shouldn’t be there, but I do agree that it needs some more work. I have some fixes that need to be applied to the code and will try to get them in soon.

  4. John
    It was a conditional statement, not an absolute one. My example has issues and they are explicitly stated. It is a matter of disclosure, under certain conditions it is acceptable.

    The problem is that there is no easy fix for the issues in that example so you are very limited in what you can do. All 17 occurrences of references to the sectionRowIndex property have to be taken care of as well as the opposite assumption, that there is a correspondence in between the record and the row, and those are harder to find out since there is no single keyword to look for, and I would assume there are just as many.

    Anyway, it was you who asked about that example, don’t complain about my reply.

  5. Touché.

    We’re actually using this extension, so I have an incentive to fix the problems. I submitted one patch for IE, and pagination is next.

  6. I am trying to use nested table and i am creating the JSON object from java side.But data is not rendering on the web page.Following is the JSON response i am able to see from firebug

    ({“query”:{“count”:”2″,”created”:”2010-03-30T02:43:40Z”,”lang”:”en-US”,”updated”:”2010-03-30T02:43:40Z”,”uri”:”http://localhost:8080/YUI/yuiAction.do?format=json&debug=true&q=”,”diagnostics”:{“publiclyCallable”:”true”,”url”:{“execution-time”:”356″,”content”:”http://us.music.yahooapis.com/release/v1/list/artist/269863?start=1&count=2″},”user-time”:”378″,”service-time”:”356″,”build-version”:”5275″},”results”:{“Release”:[{"id":"209601444","releaseYear":"2009","title":"Classic [Spectrum Audio]“,”url”:”http://new.music.yahoo.com/abc/albums/classic-spectrum-audio–209601444″}]}}})

  7. Hi Satyam

    Can you please share the nested data table example that used XHRDataSource

  8. Nicely done Satyam, I will be reviewing your technique and samples from your website in order to implement this in my app.

    Thanks!

  9. Hi!!

    Can you please share an example that uses XHRDataSource. Have tried but its just not calling the source file for json data.

    Regards.

  10. Solved NDT using XHRDataSource :-)