Thursday 31st May, 2012

Inline editing

Update: If you are looking for a complete editing solution for DataTables Editor is now available and can add editing abilities to your tables in minutes.

This blog post was written before the release of DataTables 1.10, and does not make use of the new API and features in 1.10. It is left for reference, but please refer to the manual for instructions on how to use DataTables.

In modern web-applications, one common use for tables is to show a grid of information which can be modified by the end user - a typical CRUD (Create, Read, Update, Delete) scenario. One example of this is a list of users for the web-app where the administrator can update user details on-the-fly in the table view.

There are a million and one different ways to present the editing interface for this table, and which method you wish you use will depend on how you want it to interface with the rest of your web-site / app. You might wish to have a lightbox editing form floating on top of the table, or you could use a cell at a time approach using a library like jEditable. Typically I've shied away from providing an editing plug-in for DataTables because how it should be implemented will vary significantly from situation to situation and one size does not fit all here.

Fortunately with DataTables is very easy to create a CRUD interface which is tailored to your specific setup using the DataTables API. In this example I will show how to create an interface which provides inline editing of a whole row. What we end up with at the end of this tutorial is shown below:

The table

As always we need a table as the starting point. For this I'll base the table we are going to make editable on my standard table of Conditional-CSS's browser support with two extra columns, one for an editing button and one for a delete button. These can of course be styled or customised (images etc) as you wish. I'll also add a link for creating a new row just above the table. The markup is shown below:

<p><a id="new" href="">Add new row</a></p>
<table cellpadding="0" cellspacing="0" border="0" class="display" id="example">
    <thead>
        <tr>
            <th>Rendering engine</th>
            <th>Browser</th>
            <th>Platform(s)</th>
            <th>Engine version</th>
            <th>CSS grade</th>
            <th>Edit</th>
            <th>Delete</th>
        </tr>
    </thead>
    <tbody>
        <tr class="odd gradeX">
            <td>Trident</td>
            <td>Internet Explorer 4.0</td>
            <td>Win 95+</td>
            <td class="center">4</td>
            <td class="center">X</td>
            <td><a class="edit" href="">Edit</a></td>
            <td><a class="delete" href="">Delete</a></td>
        </tr>
        ...
    </tbody>
</table>

The DataTables initialisation for this is trivial:

    $(document).ready(function() {
        var oTable = $('#example').dataTable();
    } );

Edit mode

Of the three editing functions (create, edit and delete) the edit function is the first that we will tackle here. My method for doing this is to simply replace the content of each cell in the row to be made editable with an input tag, and its value set to the content of the cell. This will not have any effect on the functions that DataTables provides, such as sorting and filtering, since DataTables maintains an internal cache of the data from the cell (for faster data access) - so we can do whatever we need to the DOM element.

The function for this is quite simple:

function editRow ( oTable, nRow )
{
    var aData = oTable.fnGetData(nRow);
    var jqTds = $('>td', nRow);
    jqTds[0].innerHTML = '<input type="text" value="'+aData[0]+'">';
    jqTds[1].innerHTML = '<input type="text" value="'+aData[1]+'">';
    jqTds[2].innerHTML = '<input type="text" value="'+aData[2]+'">';
    jqTds[3].innerHTML = '<input type="text" value="'+aData[3]+'">';
    jqTds[4].innerHTML = '<input type="text" value="'+aData[4]+'">';
    jqTds[5].innerHTML = '[Save]()';
}

As you can see the function editRow() takes two parameters:

    1. oTable - The DataTables instance
    1. nRow - The TR node for the row to be edited

For each cell which can be edited in the row we insert the input tag and set its value (through the use of fnGetData to get the data for the row). Note that the 'edit' link is also updated to now say 'save', indicating to the end user what action will be performed when it is clicked again. Obviously this is a very simple case and it can be readily be expanded to include select elements or any other kind of input you would wish to use.

function saveRow ( oTable, nRow )
{
    var jqInputs = $('input', nRow);
    oTable.fnUpdate( jqInputs[0].value, nRow, 0, false );
    oTable.fnUpdate( jqInputs[1].value, nRow, 1, false );
    oTable.fnUpdate( jqInputs[2].value, nRow, 2, false );
    oTable.fnUpdate( jqInputs[3].value, nRow, 3, false );
    oTable.fnUpdate( jqInputs[4].value, nRow, 4, false );
    oTable.fnUpdate( '[Edit]()', nRow, 5, false );
    oTable.fnDraw();
}

To save the information that has been edited by the user back into the table (so it can then be sorted and filtered as normal) we use the fnUpdate API method. fnUpdate will write the value given to the TD cell node, effectively replacing the input element that we inserted in the editRow function with the new value. Note that the forth parameter passed to fnUpdate is false to indicate to DataTables that it should not redraw the table - otherwise we would be doing a full redraw six times here, when just once, after all updates have been completed, is enough.

Now we have the edit and save functions, we need to add suitable event handlers to the document to call them. To do this we attach a live event handler to the a tags in the edit cells, which will decide what actions to take. There are three states we can be in when the 'edit' cell is clicked:

  • No row currently being edited
  • This row is being edited and should be saved
  • A different row is being edited - the edit should be cancelled and this row edited

To keep track of which row is being edited we have a variable called nEditing to store a reference to it - from this we can decide what state we are in. As a result we can create the following for our initialisation code:

$(document).ready(function() {
    var oTable = $('#example').dataTable();
    var nEditing = null;

    $('#example a.edit').live('click', function (e) {
        e.preventDefault();

        /* Get the row as a parent of the link that was clicked on */
        var nRow = $(this).parents('tr')[0];

        if ( nEditing !== null &amp;&amp; nEditing != nRow ) {
            /* A different row is being edited - the edit should be cancelled and this row edited */
            restoreRow( oTable, nEditing );
            editRow( oTable, nRow );
            nEditing = nRow;
        }
        else if ( nEditing == nRow &amp;&amp; this.innerHTML == "Save" ) {
            /* This row is being edited and should be saved */
            saveRow( oTable, nEditing );
            nEditing = null;
        }
        else {
            /* No row currently being edited */
            editRow( oTable, nRow );
            nEditing = nRow;
        }
    } );
} );

Adding rows

That's the complex part out of the way - all downhill from here! To add a new row to the table, DataTables provides the fnAddData API method. We can make use of this, in combination with the editRow function from above to create a new row and immediately place it into editing mode. Here we attach the event handler to the special link at the top of the table, inside the document ready function:

$('#new').click( function (e) {
    e.preventDefault();

    var aiNew = oTable.fnAddData( [ '', '', '', '', '', 
        '[Edit]()', '[Delete]()' ] );
    var nRow = oTable.fnGetNodes( aiNew[0] );
    editRow( oTable, nRow );
    nEditing = nRow;
} );

There are two points to consider here: firstly fnAddData returns an array of indexes, each of which points to the row information that was stored in DataTables for the new row (it's an array since fnAddData can add more than one row at a time). From this index we can use fnGetNodes to obtain the tr element to make editable. Secondly we set the nEditing variable to be the new row so the edit/save handler knows that it should be saved when clicked on.

Deleting rows

To wrap up the three editing functions we just need to add the option to delete rows from the table now, and this is easily accomplished using the fnDeleteRow API method that DataTables presents - just pass in a reference to the row to be deleted:

$('#example a.delete').live('click', function (e) {
    e.preventDefault();

    var nRow = $(this).parents('tr')[0];
    oTable.fnDeleteRow( nRow );
} );

Conclusion

In this article I've presented one possible method for how a CRUD interface can be built up in DataTables. The implementation shown here is intentionally kept simple in order to show the use of the DataTables API, and to show that this kind of interface can be readily built and customised for your site / app.

It is worth noting there are a number of limitations to the implementation, and areas where it can be improved:

  • Data is not saved to the server at the moment, just the local DataTables instance - so a reload takes the table back to its original state. An XHR call to the server would be needed to save the user input information into a database (in saveRow).
  • The number of columns and which columns can be edited is hard coded - it would be nice to be able to specify an array of column indexes noting which columns should be editable.
  • Input type is limited to just text input at the moment - options such as select inputs would be desirable.
  • UI improvements such as automatic focus and save on 'return' could be implemented to make life easier for the end user.

I rather feel that this has the beginnings of a plug-in written all over it - anyone fancy taking up the challenge of building this up into a full plug-in for DataTables? :-)

Comments and discussion on this post in the forum.