Using Handsontable Inside jQuery Tabbed Dialogs

John Boardman JavaScript, Technology Snapshot, Tutorial Leave a Comment

Update 12-9-2014  – See the end of the article for the PHP/Server Side code!

Handsontable is a minimalistic Excel-like data grid editor for HTML, JavaScript & jQuery. I came across it while looking for a ready-made, easy-to-use, and small data editor for implementing a client requirement involving loading, editing, and saving reference data inside an SPA (single page application). Handsontable met all of my requirements and more.

Additional requirements cropped up, as they usually do, to work inside a JQuery dialog, and to work inside a tab. This required negotiating around an issue (which I will cover here) but ultimately ended up working out just fine.

Git Repo

Incidentally, the git repository for this blog is https://github.com/jwboardman/hot_example.git. I created a standalone, full example for you to play with, so feel free to go get it and try it out.

Here’s a screenshot. Keep in mind I’m not much for CSS tricks and styling, so let’s focus on the functionality, shall we?

HotExample

Step 1: Display the Dialog

The first thing to do is pop up a jQuery dialog, so let’s add a placeholder to the HTML.

index.html:

 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  <title>Handsontable Example</title>
  <link href="./css/jquery-ui-1.10.4.min.css" type="text/css" rel="stylesheet"/>
  <link href="./css/style.css?version=0.0.0.1" type="text/css" rel="stylesheet"/>
  <link href="./css/jquery.handsontable.full.css" type="text/css" rel="stylesheet"/>
 </head>
 <body>
  <div class="wrapper">
   <table width="200" border="0" cellspacing="0" cellpadding="0"><tbody>
    <tr><td>
     <div class="menu">
      <ul><li class="toolbar_tools">
       <a href="#">Tools</a>
       <ul><li><a id="menu_example" href="JavaScript:void(0);">Example</a></li></ul>
      </li></ul>
     </div>
    </td></tr>
   </tbody></table>
  </div>
  <script type="text/javascript" src="./js/jquery/jquery-1.11.0.min.js"></script>
  <script type="text/javascript" src="./js/jquery/jquery-migrate-1.2.1.min.js"></script>
  <script type="text/javascript" src="./js/jquery/jquery.validate-1.11.1.min.js"></script>
  <script type="text/javascript" src="./js/jquery/jquery-ui-1.10.4.min.js"></script>
  <script type="text/javascript" src="./js/jquery/jquery.dform-1.1.0.min.js"></script>
  <script type="text/javascript" src="./js/jquery/jquery.ui.widget-1.10.4.js"></script>
  <script type="text/javascript" src="./js/jquery/jquery.tools.min.js"></script>
  <script type="text/javascript" src="./js/jquery/jquery.handsontable.full.js"></script>
  <script type="text/javascript" src="./js/example.toolbar.js?version=0.0.0.1"></script>
  <script type="text/javascript" src="./js/example.hot.js?version=0.0.0.1"></script>
  <script type="text/javascript" src="./js/example.js?version=0.0.0.1"></script>
 </body>
 </html>

With a bit of CSS the Tools–>Example would be a menu, but I’d rather get to the meat of the blog, and that is using Handsontable with jQuery dialogs.

Note the order of the last three .js files. example.js is last because it contains the call to initialize the JavaScript. The whole file is pretty short so let’s look at it:

example.js:

//Initialize
$.example.initialize = function() {
    $('#menu_example').unbind();
    $('#menu_example').click(function(e) {
        $.example.showExampleDialog();
        e.preventDefault();
    });

    // load default units
    $.example.loadExample();
};

//Page Ready
$(document).ready(function() {
    $.example.initialize();
});

Simple jQuery. When the page loads, the initialize() method sets up to handle the click on the “Example” button. $example.loadExample() calls a function that would load the user’s saved preferences if this were a real app. Here it doesn’t do much but show you where you would call it.

NOTE: I place all data and functions into the $.example namespace so the global namespace is not polluted. By doing this you can ensure that you aren’t overwriting any other global data, and you keep all of your data in one area.

When the user clicks on the “Example” button, we jump over to example.hot.js to run the $.example.showExampleDialog() function:

// This is the function that is called to display the dialog with the hot tabs
$.example.showExampleDialog = function() {
	$.example.toolbar.data.example(); // load form...
    $.example.fillExampleDialog();
}

$.example.toolbar.data.example() is a little helper that is found in example.toolbar.js:

$.example.toolbar.data = {
    'example': function() {
        $.example.toolbar.loadDialog('example', 'Example', 760, 500);
    }
};

Now why is this a good idea? I like this because it collects all of my dform-powered dialogs into one spot. I currently have 65 in my SPA, so you really have to get organized to deal with that many dialogs.

$.example.toolbar.loadDialog() takes 4 parameters. the first is the name of the form to load, the second is the title of the dialog, the third is the width of the dialog, and the 4th is the height of the dialog. Using these parameters allowed me to write a little generic function to load any dform dialog.

$.example.toolbar.loadDialog = function(name, title, width, height) {
    if (!$.example.exampleDialogs[name]) {
        if (width) {
	        $.example.exampleDialogs[name] = $('<div class="' + name + '"></div>')
                      .html('')
                      .dialog({ autoOpen:false,
                                title:title,
                                modal:true,
                                width:width,
                                height:height,
                                minWidth:width,
                                minHeight:height,
                                close: function(event, ui) {}
                      });
        } else {
            $.example.exampleDialogs[name] = $('<div class="' + name + '"></div>')
                      .html('')
                      .dialog({ autoOpen:false,
                                title:title,
                                modal:true,
                                close: function(event, ui) {}
                      });
        }

        $.example.exampleDialogs[name].dialog('open');
        $('.' + name).dform($.example.toolbar.forms[name]);
    } else {
        $.example.exampleDialogs[name].dialog('open');
    }
};

Having 65 dialogs makes you get a little creative to save duplicating code. This function will load the dform if it hasn’t been defined before, or re-open one that has already been defined. It is important to not load the same dialog multiple times so you can use unique IDs inside the form definitions and still have everything work the 2nd or subsequent time the user loads a dialog. I won’t duplicate the entire dform definition here because it is a bit long, but I will show the most important part – the divs that define the tabs and the handsontables.

{
    'type'  : 'span',
    'class' : 'tab_style_active',
    'id'    : 'color_tab',
    'html'  : 'Color'
},
{
    'type'  : 'span',
    'class' : 'tab_style',
    'id'    : 'shape_tab',
    'html'  : 'Shape'
},
{
    'type' : 'div',
    'class': 'cleardiv'
},
{
    'type'  : 'div',
    'id'    : 'color_hot',
    'class' : 'handsontable hot-contain-div',
    'style' : 'width: 620px; height: 350px; overflow: auto;'
},
{
    'type'  : 'div',
    'id'    : 'shape_hot',
    'class' : 'handsontable hot-contain-div_hide',
    'style' : 'width: 620px; height: 350px; overflow: auto;'
}

The spans that define the tabs have CSS to make them look like tabs, with tab_style_active being the selected tab. Swapping this class to whichever tab the user clicks on gives the appearance of switching tabs.

Adding the class “handsontable” to the divs is required to give the handsontable divs the styling that makes the tables look like Excel spreadsheets. The “hot-contain-div” corresponds to the table that is active, whereas the “hot-contain-div_hide” class is used to hide tables that aren’t active. This will come in useful later when the user clicks on a button outside the table that is supposed to alter the active table. Making the width and height of the divs static ensures that they will stay a consistent size when switching between them. Finally, “overflow: auto” enables scroll bars to pop up automatically if our data extends beyond the given width and height.

Step 2: Display the Tables

Okay, now the dialog is on the screen…but it isn’t very useful yet with some tabs and a couple of empty divs. $.example.fillExampleDialog() contains the code that actually creates the Handsontables and fills them with data. Here is the code to load the first set of data:

$.example.fillExampleDialog = function() {
    var i = 0;
    var data = [];

    // due to a bug in handsontable, if the div isn't visible when the table is created, it will never show scrollbars
    $.example.showExampleTab('color');
    var ht = $("#color_hot");
    if ($.example.tempColorTable) {
        for (i = 0; i < $.example.tempColorTable.length; i++) {
            data.push([$.example.tempColorTable[i].name,
                       $.example.tempColorTable[i].abbrv,
                       $.example.tempColorTable[i].rgb,
                       $.example.tempColorTable[i].keyorder]);
        }
    } else {
        for (i = 0; i < $.example.refColorTable.length; i++) {
            data.push([$.example.refColorTable[i].name,
                       $.example.refColorTable[i].abbrv,
                       $.example.refColorTable[i].rgb,
                       $.example.refColorTable[i].keyorder]);
        }
    }

As the comment says, if the table isn’t visible when it is created, nothing you can do will make those scrollbars show up. So, I call a helper function to make sure the div is visible. For my client, one of the requirements was to be able to alter the data “just for this session.” Because of that and the fact that their reference data does not change, I have a copy of the reference data statically in the JavaScript. I’ve switched to colors and shapes for this example but the concept is the same.

The $.example.tempColorTable will contain the user’s data if they have changed anything. The $example.refColorTable never changes and contains the base data that is used if the user hasn’t made any changes. Back when we started I had the placeholder for loading the user’s data. If any data is loaded, it would be in $.example.tempColortable.

Now, the Handsontable for colors is created. You’ll see the width and height duplicated from the CSS style in the dform. This is intentional and forces the table to always, always be the same size. Lets go through the other settings:

  • stretchH being set to ‘all’ allows the table to distribute the available width among the columns.
  • minSpareRows is 0 because I have “Add Row” and “Remove Row” buttons, and because I auto-set the read-only KeyOrder column when a row is created.
  • multiSelect is false because I only operate on one thing at a time
  • rowHeaders is true because it conveniently displays the row number
  • colHeaders has the columns for the table defined
  • fillHandle is false because I have no need for the same data to be copied across the table
  • contextMenu is false because there weren’t any popup menu items that i needed to have available
  • outsideClickDeselects is false because I want the cell to still be selected when the “Add Row”, “Remove Row”, “Up”, or “Down” buttons are clicked
  • columns defines the data type for each column, and whether each column can be edited
  • cells allows defining a custom renderer for table, which I use to gray out column 3 because it is read-only
  • afterCreateRow auto-sets the KeyOrder cell in a new row
ht.handsontable({
        data         : data,
        stretchH     : 'all',
        width        : 620,
        height       : 350,
        minSpareRows : 0,
        multiSelect  : false,
        rowHeaders   : true,
        colHeaders   : ["Name", "Abbrv", "RGB", "KeyOrder"],
        fillHandle   : false,
        contextMenu  : false,
        outsideClickDeselects : false,
        columns    : [
            {data: 0, type : 'text', readOnly: false},
            {data: 1, type : 'text', readOnly: false},
            {data: 2, type : 'text', readOnly: false},
            {data: 3, type : 'numeric', readOnly: true}
        ],
        cells : function(row, col, prop) {
            return {'renderer' : $.example.rowRenderer};
        },
        afterCreateRow: function(index, amount) {
            var ht = $("#color_hot").handsontable('getInstance');
            var rowData = ht.getDataAtRow(index);
            rowData[3] = index;
            ht.render();
        }
    });

The other table is similarly defined. The tabs are fairly easy to code…just simple CSS class swaps on the tab spans and Handsontable divs when the tabs are clicked. You’ll notice in the Ok, Apply, Cancel, and Reset buttons that they destroy the tables, so when the dialog is re-opened the tables are created again from scratch. This ensures the data is always current and that no memory is leaked from orphaning old tables.

Step 3: Operate on the Tables

Now we have the tables up in tabs and the data loaded. Let’s let the user alter some data!

With Handsontable, double-clicking on a cell switches to edit mode. Unfortunately, when doing that inside a jQuery dialog, there is a bug that causes the div containing the editor to appear BELOW the Handsontable instead of on top of it. Fortunately (for you!) I have debugged that problem and reported it to the author (https://github.com/warpech/jquery-handsontable/issues/1444).

Here’s how to fix the bug – change line 5017 of jquery.handsontable.full.js from this:

this.instance.rootElement[0].appendChild(this.TEXTAREA_PARENT);

to this:

this.instance.rootElement[0].firstChild.appendChild(this.TEXTAREA_PARENT);

So now the editor works! I have some buttons along the bottom. Let’s discuss how those are implemented.

Move Up:

var hotMoveUp = $('#hot_move_up');
    hotMoveUp.unbind();
    hotMoveUp.click(function(e) {
        var ht = $(".hot-contain-div").handsontable('getInstance');
        var selArray = ht.getSelected();
        if (selArray) {
            var row = selArray[0];
            if (row > 0) {
                var rowData = ht.getDataAtRow(row);
                var aboveRowData = ht.getDataAtRow(row - 1);
                var temp;

                for (var i = 0; i < rowData.length; i++) {
                    if (i != 3) {
                        temp = aboveRowData[i];
                        aboveRowData[i] = rowData[i];
                        rowData[i] = temp;
                    }
                }

                ht.deselectCell();
                ht.render();
            }
        }
        e.preventDefault();
    });

Notice that we are getting the instance of the handsontable corresponding to the “hot-contain-div”. This returns the currently displayed table. The only other trickiness in this button-click handler is to not swap the KeyOrder column (column 4…zero-based) so the KeyOrder stays constant.

By the way, ht.render() updates Handsontable’s display to match any programmatic changes to its data. Move Down is similarly implemented.

Handsontable makes it very easy to add and remove rows.

var addRow = $('#hot_add_row');
    addRow.unbind();
    addRow.click(function(e) {
        var ht = $(".hot-contain-div").handsontable('getInstance');
        ht.alter('insert_row');
        ht.render();
        e.preventDefault();
    });

Again we get the active Handsontable instance. Adding a row is just a matter of calling ht.alter(‘insert_row’);. When the row is added, the afterCreateRow handler we defined when the table was created is automatically called, allowing us to set the KeyOrder cell for the user because it is read-only. Finally, we once more tell the table to render itself so the new row is shown to the user.

Reset All is a special button. It gives the user the power to blow away all of the changes they have made and go back to the reference data.

var resetAll = $('#hot_reset_all');
    resetAll.unbind();
    resetAll.click(function(e) {
        $.example.tempColorTable = undefined;
        $.example.tempShapeTable = undefined;
        $.example.destroyExampleTables();
        $.example.fillExampleDialog();
        e.preventDefault();
    });

Clicking the button causes the temporary tables to be wiped out, and then we destroy the Handsontable instances so they can be reloaded with the reference data.

The only other interesting part of the code is $.example.updateExampleData(), where we pull the data from the Handsontable and update the user’s tables.

$.example.updateExampleData = function() {
    var i;
    var dataColor;
    var dataShape;

    var ht = $("#color_hot").handsontable('getInstance');
    $.example.tempColorTable = [];
    dataColor = ht.getData();
    for (i = 0; i < dataColor.length; i++) {
        $.example.tempColorTable.push({'name'     : dataColor[i][0],
                                       'abbrv'    : dataColor[i][1],
                                       'rgb'      : dataColor[i][2],
                                       'keyorder' : dataColor[i][3]});
    }

    ht = $("#shape_hot").handsontable('getInstance');
    $.example.tempShapeTable = [];
    dataShape = ht.getData();
    for (i = 0; i < dataShape.length; i++) {
        $.example.tempShapeTable.push({'name'     : dataShape[i][0],
                                       'abbrv'    : dataShape[i][1],
                                       'sides'    : new String(dataShape[i][2]),
                                       'keyorder' : dataShape[i][3]});
    }
}

Calling ht.getData() returns the entire set of data for a table. We can then loop through that data and create the user’s data. Now, handsontable does support directly using many different types of data directly – it is not required to only have arrays of arrays. The reason I am doing it this way is because of the requirement to support two sets of data for each table – the reference data, and the user’s local changes. In the real app I also store the user’s changes to the server.

I have (slightly altered versions of) that code commented out in the example so you can see how I am doing it (see the git repo). For the shape table, I’m also showing the concept that the “sides” member of the table needs to be in a different format than it is shown on the handsontable (string vs number). I didn’t show it in the blog, but when the data is loaded for the “sides” column it is converted to Number. These are really basic conversions, but I have to do more complex ones in the real application so I wanted to at least provide an example.

12-9-2014 Update

To load and save the data for real, I updated the JavaScript to call a little PHP code to save and load the data. It uses jQuery to POST and GET the data. Other than that, it’s pretty straight forward.

Save:

$.example.saveExampleData = function() {
    var i;
    var dataColor;
    var dataShape;

    var ht = $("#color_hot").handsontable('getInstance');
    dataColor = ht.getData();
    ht = $("#shape_hot").handsontable('getInstance');
    dataShape = ht.getData();

    // Here is what I do to save data...
    $.post('/php/ajaxserver.php', {
            "action" : "savedata",
            "colors" : JSON.stringify(dataColor),
            "shapes" : JSON.stringify(dataShape)
        },
        function (data) {
            var obj = jQuery.parseJSON(data);
            if (obj.status === 'fail') {
            } else {
                $.example.exampleDialogs.example.dialog("close");
            }
        }
    );

    $.example.exampleDialogs.example.dialog("close");
}

Load:

$.example.loadExample = function() {
    // here's what I do to load data from a php server (I replaced the URL and call for privacy)
    var formParams = "call=loaddata";

    $.ajax({
        type : 'GET',
        url : '/php/ajaxserver.php',
        async : false,
        data : formParams,
        complete : function(data) {
            data.responseText = data.responseText.replace(/\\/g, '');
            var obj = jQuery.parseJSON(data.responseText);
            if (obj.status === 'fail') {
            } else {
                var i;
                $.example.tempColorTable = [];
                var dataColor = obj.colors;
                for (i = 0; i < dataColor.length; i++) {
                    $.example.tempColorTable.push({'name' : dataColor[i][0], 'abbrv' : dataColor[i][1], 'rgb' : dataColor[i][2], 'keyorder' : dataColor[i][3]});
                }

                $.example.tempShapeTable = [];
                var dataShape = obj.shapes;
                for (i = 0; i < dataShape.length; i++) {
                    $.example.tempShapeTable.push({'name' : dataShape[i][0], 'abbrv' : dataShape[i][1], 'sides' : dataTemperature[i][2], 'keyorder' : dataTemperature[i][3]});
                }
            }
        }
    });
};

PHP

I just added the PHP side to the example. I tested it on a Mac with Apache. If you don’t know how to get Apache going with PHP, there are a bunch of tutorials out there. Here’s just one in case you don’t want to google it (apache php mac yosemite). http://coolestguidesontheplanet.com/get-apache-mysql-php-phpmyadmin-working-osx-10-10-yosemite/.

Once Apache/PHP are up, copy the entire project to the /Library/WebServer/Documents directory. The only extra thing you’ll have to do (and this is a very simple example so it just saves data in a text file) is ‘sudo chmod 777 data’ on the data directory so it is writable.

I’m including the PHP code here:

<?php

header('Expires: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
header('Content-type: application/json; charset=utf-8');

if (isset($_GET['call'])) {
    if ($_GET['call'] == 'loaddata') {
        echo loaddata();
    }
} else if ($_POST['action'] == 'savedata') {
    echo json_encode(savedata($_POST['colors'], $_POST['shapes']));
}

function savedata($colors, $shapes) {
    $directory = "../data";
    $valid = true;
    $result = '{"status":"fail","error":"Data file not found"}';

    if (is_dir($directory)) {
        $fname = "$directory/data.txt";
        $fh    = fopen($fname, 'w');
        $data  = '"colors":' . $colors
              . ',"shapes":' . $shapes;
        fwrite($fh, $data);
        fclose($fh);
    } else {
        $result = '{"status":"fail"';
        $result .= ',"error":"Directory not found"}';
        $valid = false;
    }

    if ($valid) {
        $result = '{"status":"success"}';
    }

    return $result;
}

function loaddata() {
    $directory = "../data";
    $data = null;
    $result = '{"status":"fail","error":"Data file not found"}';

   if (is_dir($directory)) {
   $fname = "$directory/data.txt";
   if (is_file($fname)) {
            $data = loadDataFile($fname);
            if ($data != '') {
                $result = '{"status":"success",' . $data . '}';
            }
       }
   }

    return $result;
}

function loadDataFile($fileName) {
    $arr  = '';
    $file = null;</pre>
    $file = @fopen($fileName, "r");

    if ($file === false) {
        return "";
    }

    while (!feof($file)) {
        $arr .= fgets($file);
    }

    fclose($file);
    return $arr;
}
?>

Again, everything is in the git repo so go download it and have fun learning this super cool library!

Conclusion

I hope you got a feel for how useful Handsontable is and how easy it is to integrate into a jQuery dialog. I tried to share a few tips and tricks along the way that have made my coding a little easier. If you see anything that doesn’t make sense or that I could do better, please leave some feedback.

Happy coding!

— John Boardman, asktheteam@keyholesoftware.com


About the Author
John Boardman

John Boardman

Twitter

John is a Sr. Keyhole Consultant with 20+ years of experience in C, C++, Java, and IoT enterprise software design and development. Also currently writing a multi-platform, multi-user game in Unity3d (and server in Java) and have written custom graphical game engine clients in C and C++ on several platforms.


Share this Post

Leave a Reply