Example: Master and detail tables

Demonstrates a method of linking two DataTables together. In this case we link a row selection within a Master (or "parent") table to creation of a separate Detail (or "child") table. This is a common usage case for datasets that may have related rows within different datasets or as a result of typical database one-to-many key relationships.

Sample Data

Let's assume we have an array of data that includes parent elements and children elements. The example we'll use defines several animal categories and for each category it provides the names of some common characters from literature or pop culture of each type (except for the lowly amoeba, we couldn't think of any ...).

var animal_data = [
    {  aname: 'Lions',  chars:[ 'Leo', 'Simba', 'Elsa', 'Cowardly Lion' ] },
    {  aname: 'Amoebas' },
    {  aname: 'Tigers', chars:[ 'Shere Kahn', 'Tigger', 'Tony' ] },
    {  aname: 'Mules',  chars:[ 'Francis' ] },
    {  aname: 'Bears',  chars:[ 'Smokey', 'Reginald', 'Winnie-the-Pooh', 'Baloo', 'Yogi' ] },
    {  aname: 'Snakes', chars:[ 'Kaa', 'The Serpent', 'Nagini' ] }
];

The DataTables

Two DataTables are utilized for this example and for convenience they operate using the same animal_data JavaScript array. In most practical applications the data would probably be received from a remote source via DataSource or using the Model sync capability.

The "Master" table

Our primary DataTable consists of two columns, aname which is the category of the animals and the other column is a calculated (or "unbound") column that is populated by a custom formatter. The custom formatter for nchars simply returns the length of the chars array associated with the record, or zero if none are defined.

var dt_master = new Y.DataTable({
    columns : [
        { key:'aname',  label:'Type' },
        { name:'nchars', label:'No. of Chars',
          formatter: function(o){
               return ( o.data.chars ) ? o.data.chars.length : 0;
             }
        }
    ],
    data : animal_data,
    width: 200,
    caption: 'Select an animal category below:'
}).render("#mtable");

Since we will need a click handler to track TR clicks on the Master DataTable, we will define a new attribute selectedRow and setup a TR click handler that assigns this attribute on a click.

//
// Add a new attribute to track the last TR clicked,
//   this is used in the details DT formatter below and later
//   in the row click handler `delegate` for row highlighting
//
//  also setup a click listener to update the "selectedRow" attribute on TR clicks
//
dt_master.addAttr("selectedRow", { value: null });

dt_master.delegate('click', function (e) {
    this.set('selectedRow', e.currentTarget);
 }, '.yui3-datatable-data tr', dt_master);

The "Detail" table

We can proceed with defining the linked child table and rendering it initially because we have hidden this DataTable within a DIV with style display:none; (the DIV becomes visible on the first row click). This child DataTable consisits of another calculated (i.e. unbound) column aname (which just fills with the parent category name) and a column char_name. The data for this table is initially empty, but will be populated by the click handler.

var dt_detail = new Y.DataTable({
    columns : [
        { name:'aname', label:'Animal Category',
          formatter: function(o){
                // just retrieve the selected Master record and return the "aname" column
                var parent_rec = dt_master.getRecord( dt_master.get('selectedRow') );
                return parent_rec.get('aname');
            }
        },
        { key:'char_name', label:'Character' }
     ],
    data : [],
    strings : {
        emptyMessage : "No critter characters were found!"
    },
    width: 350,
    caption: 'Characters of the category include:'
}).render("#dtable");

The selectedRow Listener

The "glue" between the master and detail DataTables is the delegated click handler on the Master DataTable's rows -- or more specifically, the selectedRowChange event handler. When a row is clicked and the selectedRow is changed, the underlying record from the Master table is determined and the Detail DataTable is populated with the corresponding chars data from the clicked record. We also handle TR highlighting for the clicked row by toggling a background color within this delegate handler.

dt_master.after('selectedRowChange', function (e) {

    var tr = e.newVal,              // the Node for the TR clicked ...
        last_tr = e.prevVal,        //  "   "   "   the last TR clicked ...
        rec = this.getRecord(tr);   // the current Record for the clicked TR

    //
    //  This if-block does double duty,
    //  (a) it tracks the first click to toggle the "details" DIV to visible
    //  (b) it un-hightlights the last TR clicked
    //
    if ( !last_tr ) {
        // first time thru ... display the Detail DT DIV that was hidden
        Y.one("#chars").show();
    } else {
        last_tr.removeClass("myhilite");
    }

    //
    //  After unhighlighting, now highlight the current TR
    //
    tr.addClass("myhilite");


    //
    //  Collect the "chars" member of the parent record into an array of
    //  objects  with property name "aname"
    //
    var detail_data = [];
    if ( rec.get('chars') ) {
        Y.Array.each( rec.get('chars'), function(item){
            detail_data.push( {char_name:item});
        });
    }

    //
    //  Set the "detail_data" to the dt_detail DataTable
    //    also update the heading in "acategory"
    //   ( it automatically refreshes )
    //
    dt_detail.setAttrs({
        data: detail_data,
        caption: 'Characters of the <strong>' + rec.get('aname') +
                    '</strong> category include:'
    });
});

Note: In the case of the use of remote data via DataSource, the selectedRowChange handler could be modified to generate a sendRequest or similar remote call for the Detail data and the on:success handler could be setup to set the data attribute.

Full Source Code

CSS

.yui3-skin-sam .yui3-datatable-caption {
    font-size: 13px;
    font-style: normal;
    text-align: left;
}

.yui3-datatable-col-nchars {
    text-align: center;
}

.yui3-skin-sam .yui3-datatable tr.myhilite td {
    background-color: #C0ffc0;
}

#mtable tbody tr {      /*  Turn on cursor to show TR's are selectable on Master DataTable only  */
    cursor: pointer;
}

HTML Markup

Note: be sure to add the yui3-skin-sam classname to the page's <body> element or to a parent element of the widget in order to apply the default CSS skin. See Understanding Skinning.

<div id="template" class="yui3-skin-sam dt-example yui3-g"> <!-- You need this skin class -->
    <div class="yui3-u-1-3" id="mtable"></div>

    <!-- This is the HTML section for the "Details" markup ...
         NOTE: it is hidden initially !!   -->
    <div class="yui3-u-2-3" id="chars" style="display:none;">
        <div id="dtable"></div>
    </div>
</div>

Javascript

YUI().use(   "datatable", function (Y) {

    var animal_data = [
        {  aname: 'Lions',  chars:[ 'Leo', 'Simba', 'Elsa', 'Cowardly Lion' ] },
        {  aname: 'Amoebas' },
        {  aname: 'Tigers', chars:[ 'Shere Kahn', 'Tigger', 'Tony' ] },
        {  aname: 'Mules',  chars:[ 'Francis' ] },
        {  aname: 'Bears',  chars:[ 'Smokey', 'Reginald', 'Winnie-the-Pooh', 'Baloo', 'Yogi' ] },
        {  aname: 'Snakes', chars:[ 'Kaa', 'The Serpent', 'Nagini' ] }
    ];

    //
    //   Create the "parent" DataTable
    //
    var dt_master = new Y.DataTable({
        columns : [
            { key:'aname',  label:'Type' },
            { name:'nchars', label:'No. of Chars',
              formatter: function(o){
                   return ( o.data.chars ) ? o.data.chars.length : 0;
                 }
            }
        ],
        data : animal_data,
        width: 200,
        caption: 'Select an animal category below:'
    }).render("#mtable");

    //
    // Add a new attribute to track the last TR clicked,
    //   this is used in the details DT formatter below and later
    //   in the row click handler `delegate` for row highlighting
    //
    //  also setup a click listener to update the "selectedRow" attribute on TR
    //  clicks
    //
    dt_master.addAttr("selectedRow", { value: null });

    dt_master.delegate('click', function (e) {
        this.set('selectedRow', e.currentTarget);
     }, '.yui3-datatable-data tr', dt_master);

    //
    //   Create the characters DataTable and render it (it is hidden initially)
    //
    var dt_detail = new Y.DataTable({
        columns : [
            { name:'aname', label:'Animal Category',
              formatter: function(o){
                    // just retrieve the selected Master record and return the
                    // "aname" column
                    var parent_rec = dt_master.getRecord(
                        dt_master.get('selectedRow') );

                    return parent_rec.get('aname');
                }
            },
            { key:'char_name', label:'Character' }
         ],
        data : [],
        strings : {
            emptyMessage : "No critter characters were found!"
        },
        width: 350,
        caption: 'Characters of the category include:'
    }).render("#dtable");

    //
    //  Setup a listener to the Master "selectedRowChange" event (i.e. after a
    //  row click)
    //
    dt_master.after('selectedRowChange', function (e) {

        var tr = e.newVal,              // the Node for the TR clicked ...
            last_tr = e.prevVal,        //  "   "   "   the last TR clicked ...
            rec = this.getRecord(tr);   // the current Record for the clicked TR

        //
        //  This if-block does double duty,
        //  (a) it tracks the first click to toggle the "details" DIV to visible
        //  (b) it un-hightlights the last TR clicked
        //
        if ( !last_tr ) {
            // first time thru ... display the Detail DT DIV that was hidden
            Y.one("#chars").show();
        } else {
            last_tr.removeClass("myhilite");
        }

        //
        //  After unhighlighting, now highlight the current TR
        //
        tr.addClass("myhilite");


        //
        //  Collect the "chars" member of the parent record into an array of
        //  objects  with property name "aname"
        //
        var detail_data = [];
        if ( rec.get('chars') ) {
            Y.Array.each( rec.get('chars'), function(item){
                detail_data.push( {char_name:item});
            });
        }

        //
        //  Set the "detail_data" to the dt_detail DataTable
        //    also update the heading in "acategory"
        //   ( it automatically refreshes )
        //
        dt_detail.setAttrs({
            data: detail_data,
            caption: 'Characters of the <strong>' + rec.get('aname') +
                        '</strong> category include:'
        });
    });

});