Working with the YUI DataTable (Updated for v2.6.0), Part 2: Changing the Contents of the DataTable

By YUI TeamOctober 27th, 2008

Don’t miss Part One of this series, in which Satyam explores practical steps on getting started with the YUI DataTable Control.

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 YDN-JavaScript developer forum.

In a previous article I wrote about how to get started with your own implementation of the DataTable component. In this article I will cover how to change the contents of the DataTable, especially how to communicate with your database server to make changes and, if successful, show those changes to the user, since in many applications, the contents of the DataTable should be a reflection of the information in a database.

Deleting records

We will start with the easiest transaction: deleting a single record.
First let’s make a column containing a delete icon which, when clicked,
will delete the row. To do that, we declare that extra column in the
column definitions for the DataTable in this way:

var myColumnDefs = [
    ....,
    ....,
    {key:'delete', label:' ', className:'delete-button'}, 
    ....
];

    
// where delete-button can be declared as:
.delete-button {
    cursor: pointer;
    background: transparent url(images/delete.png) no-repeat center center;
    width: 16px;  height: 16px;
}

We are declaring an extra column with key equal to
'delete', with a blank column header (if label
is not declared the header will default to the key value so
we have to explicitly say we don’t want one) and with a
className with the CSS attributes that you can see declared
right below. We don’t need to add any actual contents to the cell nor
assign it any onClick or id attributes We
couldn’t care less for the contents of that cell, meaning it can be an
<img>, <button>, a simple text or
nothing. The best option, though, is leave the graphics design completely
open as we have done.

We are going to be able to detect a click on the cell by subscribing to
the cellClickEvent. If we also want to have in-line cell
editing, as is often the case, we would have the following somewhere in
our code:

myDataTable.subscribe('cellClickEvent',myDataTable.onEventShowCellEditor);

This is telling the DataTable that when it receives a cell
click event it should activate the cell editor, but that is not the only
option, our ‘delete’ button can also be clicked and we want to respond to
each event accordingly. We would then have:

myDataTable.subscribe('cellClickEvent',function(oArgs) {
    var target = oArgs.target;
    var column = myDataTable.getColumn(target);
    if (column.key == 'delete') {
        if (confirm('Are you sure?')) {
            myDataTable.deleteRow(target);
        }
    } else {
        myDataTable.onEventShowCellEditor(oArgs);
    }
});

Instead of simply showing the cell editor, first we check whether the
click came from a cell in the new column, the one named ‘delete’. We use
the target cell received in the argument to get the column via
getColumn() and then check for its key value.
If the key of the clicked column is 'delete'
and if the user confirms the action we call deleteRow() and
pass in the target cell as an argument, otherwise, we leave the cell
editor pop up.

The event listener is executed in the scope of the table, so
this and myDataTable within the function are
the same, we used the latter for clarity.

Code similar to the above can be used at any time when we might want to
edit some cells and not others. Here, we have selected by column but we
can also read the record where the click happened and decide whether to
edit or not based on the value on an associated column for that same
record.

We have one problem with this code: the server knows nothing of this
change and we want to keep the server always in sync, so we will have to
insert some code in between the user confirmation and the actual row
deletion:

myDataTable.subscribe('cellClickEvent',function(oArgs) {
    var target = oArgs.target;
    var column = this.getColumn(target);
    if (column.key == 'delete') {
        if (confirm('Are you sure?')) {
            var record = this.getRecord(target);
            YAHOO.util.Connect.asyncRequest(
                'GET',
                'myServer.php?deleteRow=' + record.getData('id'),
                {
                    success: function (o) {
                        if (o.responseText == 'Ok') {
                            this.deleteRow(target);
                        } else {
                            alert(o.responseText);
                        }
                    },
                    failure: function (o) {
                        alert(o.statusText);
                    },
                    scope:this
                }
            );
        }
    } else {
        this.onEventShowCellEditor(oArgs);
    }
});

After confirming the user intention, we call asyncRequest()
of the Connection
Manager
. We make a GET request to the server, passing it
the value of the database primary key, here called id, for
the current record which we get through a call to getData().
Upon a successful reply from the server, if it sends an 'Ok'
message, we delete the row as before. If there is any other reply from
the server, we assume it to be an error message and we show it to the
user, just as we do if we are unable to connect the server in the
failure callback option.

In this example I changed the references from myDataTable to
this, since the event listener is called within the scope of
the DataTable, but beware, the callback to the
asyncRequest() method has a different scope unless you
explicitly force it and so we need to add scope:this for the
this.deleteRow() inside the callback to work. Thanks to
closure, the variable target within the success
callback function refers to the one in the enclosing function. We are not
really concerned with aesthetics here, but in your own code you might
prefer to use YAHOO.widget.SimpleDialog instead of
confirm() and alert().

If you analyze this function you may notice that all the information it
needs comes from the event target except for two hard coded
values: the column key and the database’s primary key for the record.
This forces us to rewrite this very same function for each and every
table. We can, though, provide this information from elsewhere so that we
could put this function into our own handy personal library. In the
"http://yuiblog.com/blog/2008/10/15/datatable-260-part-one/">previous
article
I mentioned that the column definitions were extensible, and
you can add your own properties to them as long as their names don’t
collide with those the YUI library uses. We can add a couple of such
properties. One we will call action and in this case we will
set action:'delete'. The other property we will call
isPrimaryKey and it will either be true or
simply not be there, hence false. We will set it to
true only in the column or columns which are the primary
keys in the database. Our generic onCellClick function might
then look like this:

var onCellClick = function(oArgs) {
    var target = oArgs.target,
        column = this.getColumn(target),
        record = this.getRecord(target);
    switch (column.action) {
        case 'delete':
            if (confirm('Are you sure?')) {
                
                YAHOO.util.Connect.asyncRequest(
                    'GET',
                    'myServer.php?action=delete' + myBuildUrl(this,record),
                    {
                        success: function (o) {
                            if (o.responseText == 'Ok') {
                                this.deleteRow(target);
                            } else {
                                alert(o.responseText);
                            }
                        },
                        failure: function (o) {
                            alert(o.statusText);
                        },
                        scope:this
                    }
                );
            }
            break;
        default:
            this.onEventShowCellEditor(oArgs);
            break;
    }
};

// which we would use like this:
myDataTable.subscribe('cellClickEvent',onCellClick);

We intentionally left the door open to accept other actions within the
same function, the default always being editing cells. This does not mean
that all cells will be editable, onEventShowCellEditor first
checks whether the column has the editor attribute set. The
myBuildUrl() function is:

var myBuildUrl = function(datatable,record) {
    var url = '';
    var cols = datatable.getColumnSet().keys;
    for (var i = 0; i < cols.length; i++) {
        if (cols[i].isPrimaryKey) {
            url += '&' + cols[i].key + '=' + escape(record.getData(cols[i].key));
        }
    }
    return url;
};

This function goes through all the column definitions looking for the
isPrimaryKey property and, if set, it will concatenate into
the variable url the column’s key name, which
we assume to be URL-safe, and the current value for that column taken
from record all in proper and escaped URL-encoded format.
The value returned will even have the leading '&' since,
most of the time, we will be appending it to a partially built URL. Most
of the transactions we do with the server will require us to identify the
record by its primary keys so we will use this function quite often.

Inserting rows

It is quite easy to insert a blank row into the DataTable.
Let’s assume that we have added another column that has buttons to insert
a row. The following code will insert a blank row above the row where the
button was clicked. Since we have left the switch on
column.action open for expansion we now add:

case 'insert':
    this.addRow( {} , this.getRecordIndex(target));
    break;
    ....

The addRow() method takes an object literal whose property
names are those of the column.key properties and the values
for each property are the ones to be inserted. Those fields not
explicitly set (none in this example) will be set to
undefined. In normal circumstances the object literal should
be filled with suitable defaults. The second argument to
addRow() is the position where the record is to be inserted,
in this case taken from the position of the click via
getRecordIndex().

The user will still need to input data for the new row and the new data
will need to be sent to the server as we will see later on. Since there
is no built-in row-editing functionality in the DataTable, the best
option is to pop up a Dialog from the Container component.
The "http://developer.yahoo.com/yui/container/dialog/">documentation for the
Dialog component
shows how to assemble a form and submit the
information collected to the server, so I won’t cover that here.

Another idea for row insertion is to trigger it via a context menu, where
you would have to insert the new row in relation to the row that
triggered the context menu. Or an ‘Add New Record’ button or link
directly outside the table could cause a row to be inserted at position
0, which would be the top of the table. This is a case where we can fully
appreciate the difference in between row numbers and record id numbers
that we mentioned in the "http://yuiblog.com/blog/2008/10/15/datatable-260-part-one/">previous
article
. The record we have just created will get a new record id
number, one higher than the highest record ever assigned. Nevertheless,
it would get row number 0, because that’s where we placed it with the
addRow() method. They are different concepts and they are
also different from the primary key(s) of the database record on the
server.

Once we decide how we want to allow the user to insert a row, we need to
send the request to the server and handle its reply. We should not fully
count on the information the user entered into the Dialog
form — the database might assign default values for empty fields, it
might have timestamp values inserted into date fields or it might have an
auto-incremented integer as its primary key so we need to know what that
primary key value turned out to be. In the "http://developer.yahoo.com/yui/container/dialog/#submit">Submitting Form
Data
section in the Dialog documentation we have the
onSuccess callback function which simply pops up a message
alert to the user. We will use that callback to insert the record with
the values returned from the server.

In the "http://yuiblog.com/blog/2008/10/15/datatable-260-part-one/">previous
article
I mentioned the benefits of using a standard message
response. We didn’t use that standard message format in the previous
section in order to focus on the new information presented there. Let’s
take a moment and review it since we now have a more complex reply from
the server and we can’t just get by with a plain ‘Ok’. I use JSON as the
basis for my server replies; I don’t recommend plain text because it
lacks any structure (i.e. you could not make an envelope); and though XML
is a good choice, I find it too verbose and harder to parse. Three
possible messages in my message format are:

// Plain Ok response
{   "replyCode":200, "replyText":"Ok" }

// Ok with extra data
{
    "replyCode":201, "replyText":"Data Follows", 
    "data":[{ ... } , ... ] 
}

// Something went wrong
{ "replyCode":500, "replyText":"Something really bad happened" }

All messages will be enclosed in an envelope carrying status information
which reports in numeric and textual form the result of the transaction
requested. Reply codes in the 2xx range mean no error. A 200 is an Ok
reply when there is no extra information to be provided, a 201 is a
variation meaning that there is extra data in the reply. Codes in the 5xx
range and above mean some sort of error. The numerical values for the
replyCode are modeled after the standard HTTP reply codes
but they should not be confused with them and no attempt should be made
to send those reply codes in the HTTP headers. Browsers, proxy servers
and other intermediaries may act on standard HTTP reply codes and they
might get confused if we send our application-level codes at the
communication level, where they do not belong.

In the row delete case there would either be a successful 200
'Ok'
reply or it would get some 5xx replyCode and an
error message in replyText. For a data fetch (i.e.:when
filling a DataTable) the reply would have a 201 code and the
data property would contain the data requested. In our row
insert case, we would receive the reply status envelope plus the full
record with all its default values, auto-incremented fields, timestamps
and whatnot or, in the case of server error, a 500 replyCode
and an error description.

Thus, our Dialog submit onSuccess callback function would
look like this:

var onSuccess = function(o) {
    var r = YAHOO.lang.JSON.parse(o.responseText, function (k,v) {
        if (/-date$/.test(k) {
            return YAHOO.util.DataSource.parseDate(v);
        }
        return v;
    });
    if (r.replyCode == 201) {
        myDataTable.addRow(r.data, 0);
        var tr = myDataTable.getTrEl(0);
        tr.scrollIntoView();
        YAHOO.util.Dom.addClass(tr, 'my-highlight-row');
        setTimeout(function() {
            YAHOO.util.Dom.removeClass(tr, 'my-highlight-row');
        }, 2000);
    } else {
        alert(r.replyText);
    }
}:

We first decode the JSON reply with the YUI "http://developer.yahoo.com/yui/json/">JSON utility. Since we
presumably have agreed that all date fields will end with the
-date suffix we use the reviver, a function passed
as the second argument to the parser, to detect them, via a suitable
regular expression and convert then to JavaScript dates. The converter we
use is the very same parser the DataSource uses so we should get the same
results as the DataTable gets. If replyCode is
201, indicating success plus data, we add the row just as it
comes from the server in the first table position, row number 0. Though
YUI’s JSON parser offers the alternative to parse values on the fly,
other parsers for other message formats might not include such a facility
and it might be cumbersome to specify a reviver for all possible
data types, so it is more likely we wouldn’t use it and instead the call
to addRow() in a real case would look like this:

myDataTable.addRow({
    someNumericField: YAHOO.util.DataSource.parseNumber(r.data.someNumericField),
    someDateField: YAHOO.util.DataSource.parseDate(r.data.someDateField),
    someTextField: r.date.someTextField,
    someBooleanField: !!r.date.someBooleanField  // yes, those are two 'not' operators put together
},0);

The lines after the call to addRow() simply use the Dom
Utility’s addClass() and removeClass() to
highlight the row for 2 seconds in order to call attention to the newly
inserted row, which is brought into view by the HTML DOM function
scrollIntoView.

All along we’ve been assuming the column key names are the
same as the database table column names, a practice I recommend. SQL
names are valid JavaScript variable names and are URL-safe so there is no
reason not to use the same names all through, but of course, if you
inherit a system with funny column names (some databases allow names in
national language character sets which might cause encoding problems or
JavaScript errors) you might have to change those names, preferably on
the server side, if that is where the culprit lies.

In-line cell
editing

One of the most attractive features of the DataTable is that
you can edit cells as if in a spreadsheet. The "http://developer.yahoo.com/yui/datatable/#edit">documentation covers
the basics and a working example can be found "http://developer.yahoo.com/yui/examples/datatable/dt_cellediting.html">here.
Besides setting the editor property in the column
definitions to a suitable editor for that column, you have to remember to
pass the cellClickEvent to the
onEventShowCellEditor() method just as is shown in the
documentation or how we did when we captured the
cellClickEvent for our own use.

There have been several important changes for 2.6.0. In previous
versions, the editor property of a column definition would
take a short-name such as 'textbox' or the full reference to
the plug-in for that type of field such as
YAHOO.widget.DataTable.editCheckbox. Though these two ways
are still supported for backward compatibility, each type of editor is
now a separate class, all inheriting from BaseCellEditor so
this is how it looked like and how you would do it now (taken from the
"http://developer.yahoo.com/yui/examples/datatable/dt_cellediting.html">cell
editing example
):

// Sample of old style
{key: "amount", editor: "textbox", editorOptions: {validator: YAHOO.widget.DataTable.validateNumber}},
{key: "active", editor: "radio", editorOptions: {radioOptions: ["yes", "no", "maybe"], disableBtns: true}},
{key: "colors", editor: "checkbox", editorOptions: {checkboxOptions: ["red", "yellow", "blue"]}},
                        
// Sample of new style:
{key: "amount", editor: new YAHOO.widget.TextboxCellEditor({validator: YAHOO.widget.DataTable.validateNumber})},
{key: "active", editor: new YAHOO.widget.RadioCellEditor({radioOptions: ["yes", "no", "maybe"], disableBtns: true})},
{key: "colors", editor: new YAHOO.widget.CheckboxCellEditor({checkboxOptions: ["red", "yellow", "blue"]})},

In the current version you create an instance of each kind of editor for
each column that is to be edited and provide the editor options as
arguments to the constructor. The options are mostly the same ones as
before. Back then you placed them in a separate
editorOptions property, now they are part of the object
instantiation. There is one difference in the options. For radio buttons,
checkboxes and dropdowns you could specify the options via an array with
simple textual options, as in the example above or an array of sets of
texts to be shown to the user and values to be stored internally. In
previous versions there was no agreement in the property names for those
options, now it has been standardized across all those controls in
value and label (it used to be
value and text for dropdowns),

A second major change is the possibility to confirm asynchronously the
changes against the server. Once the user accepted the change in the cell
editor, there were several ways to get notified that a change was taking
place, even to validate it and rejected, but all these happened
synchronously, with no break to be able to get an Ok from the server
before showing it to the user. We might be able to revert it afterwards,
but the user would have seen the change in the screen and would be
entitled to assume the change is valid. By the time a rejection from the
server arrived, the user might well be looking elsewhere or even
navigated away from the page. This issue occupied a big part of the
previous version of this same article, with many lines of code devoted to
it.

This has changed thanks to the asyncSubmitter setting of the
cell editors. The asyncSubmitter is a function that receives
two arguments, the first one a callback function it should call when it
gets the confirmation back from the server, the second, the new value for
that cell. The asyncSubmitter is called in the scope of the
editor so it can retrieve any other information it might require from
there, such as the Record instance, the value belongs to or the original
value of the cell. The DataTable will be blocked prior to calling the
asyncSubmitter and will be unblocked by the callback so
there is not chance for the user to go away. The cell editor is not
dismissed until the callback is called. The callback should receive one
or two arguments. The first one a boolean value to indicate success, the
second, the value that will actually end up stored in the
Record, thus allowing the server to change it. Here is a
sample of a possible submitter:

{key:'name', editor: new YAHOO.widget.TextboxCellEditor({
    asyncSubmitter: function (callback, newValue) {
        var record = this.getRecord(),
            column = this.getColumn(),
            oldValue = this.value,
            datatable = this.getDataTable();
        YAHOO.util.Connect.asyncRequest(
            'POST',
            'People.php', 
            {
                success:function(o) {
                    var r = YAHOO.lang.JSON.parse(o.responseText);
                    if (r.replyCode == 201) {
                        callback(true, r.data);
                    } else {
                        alert(r.replyText);
                        callback();
                    }
                },
                failure:function(o) {
                    alert(o.statusText);
                    callback();
                },
                scope:this
            },
            'action=cellEdit&column=' + column.key + '&newValue=' + 
                escape(newValue) + '&oldValue=' + escape(oldValue) + 
                myBuildUrl(datatable,record)
        );                                              
    }
})},

Here we have a column definition for an editable field via a plain
TextboxCellEditor which has a single argument for its
constructor, an asyncSubmitter. The submitter function
receives two arguments, the callback function and the
newValue just entered and possibly modified by the
validator (none in this case). To start with, the submitter
reads several important pieces to be able to do its job. The
record and column the cell being edited belongs
to, the original value for the field and a reference to the datatable.
Notice that the scope of the submitter is not the DataTable but the cell
editor so this is not the DataTable instance, as we are used
to from the event listeners we’ve used so far.

We then use asyncRequest to connect to the server via POST
using a very long url-encoded post document containing all of the data,
the action requested from the server, the column the cell belongs to, the
new and old values and through myBuildUrl the primary key of
the record in the server database table. When the server replies, if the
code is a 201 it means the server gave an Ok and it is sending the
possibly fixed value back, we then call the callback function signaling
the acceptance and what the value actually stored should be. For any
other reply, we show an alert and call the callback with no arguments
(the first one will equate to false when missing and the second argument
is irrelevant in this case). Placing a breakpoint with a debugger in this
code anywhere before it calls back the cell editor will show the
DataTable greyed out and completely blocked.

A detail often overlooked is that the validator function is
also the place to do data conversion for the data just entered before it
is sent to the server. The value finally saved into the table is not the
one initially read into the variable newData but that
returned by the validator function. This is not optional for
some data types. Digits entered into an HTML textbox do not make a number
but a string of digits. If we allow that string to get into the
RecordSet, we will find, later on, that sorting columns
doesn’t work as expected. Other cell editors don’t give trouble: the
calendar editor will already return a native JavaScript Date
object. A dropdown will return the option value, not the description but,
once again, as a string not a number. DataTable has the
static function YAHOO.widget.DataTable.validateNumber which
can be assigned to the validator function for any numeric
column to do such conversion, as seen in the "http://developer.yahoo.com/yui/examples/datatable/dt_cellediting.html">example
(see the column definition for column 'amount').

"Checkboxes_and_radio_buttons">Checkbox and radio button editors

We have already seen how to use the formatters for these two types of
input elements. The corresponding editors allow multiple checkboxes or
radio buttons in the same cell. Their functionality is no different than
having a dropdown with multiple or single selection respectively, they
are not meant to edit a single boolean value per cell, though they could,
but it is cumbersome for the user to have to click once to bring the
editor up, a second time for the choice and then a third click on the Ok
(unless you used the disableBtns option). The use of similar
names is somewhat misleading in that you might associate
formatCheckbox with editCheckbox and
formatRadio with editRadio but they are not at
all related, the data they represent is completely different, the
formatters represent booleans, the editors single or multiple choices
within enumerations.

In the "http://yuiblog.com/blog/2008/10/15/datatable-260-part-one/">previous
article
we have seen how to respond to click events in checkboxes and
radio buttons but we have not used them to update the Recordset. Besides,
we have always gone by the principle that the server should be notified
of the intended change and upon approval we would update what the user
sees. The following code shows how we can do it:

myDataTable.subscribe('checkboxClickEvent', function(oArgs){
    // hold the change for now
    YAHOO.util.Event.preventDefault(oArgs.event);
    // block the user from doing anything
    this.disable();

    // Read all we need
    var elCheckbox = oArgs.target,
        newValue = elCheckbox.checked,
        record = this.getRecord(elCheckbox),
        column = this.getColumn(elCheckbox),
        oldValue = record.getData(column.key),
        recordIndex = this.getRecordIndex(record),
        recordKey = record.getData('recordKey');

    // check against server
    YAHOO.util.Connect.asyncRequest(
        'POST',
        'People.php', 
        {
            success:function(o) {
                var r = YAHOO.lang.JSON.parse(o.responseText);
                if (r.replyCode == 200) {
                    // If Ok, do the change
                    var data = record.getData();
                    data[column.key] = newValue;
                    this.updateRow(recordIndex,data);
                } else {
                    alert(r.replyText);
                }
                // unblock the interface
                this.undisable();
            },
            failure:function(o) {
                alert(o.statusText);
                this.undisable();
            },
            scope:this
        },
        // data to be sent to the server
        'action=cellEdit&column=' + column.key + '&newValue=' + 
            escape(newValue) + '&oldValue=' + escape(oldValue) + 
            myBuildUrl(this,record)
    );                                              
});

In this case we first prevent the checkbox from changing state because we
don’t want that to happen until we get an Ok from the server, thus we
call preventDefault. The DataTable now has the ability to
disable itself so the user won’t be able to continue while the request to
the server is processed. This is done automatically by the cell editors,
but we have to handle it ourselves in this case so we call method
disable before submitting the information to the server and
call undisable after the reply has arrived.

We then read a bunch of values and objects that we need to report to the
server or to confirm the update. As with the previous example, we
assemble a big URL with all this information and send it to the server.
Upon receiving the server reply, if it is a 200 code we use
updateRow to have both the UI and the recordset updated at
once.

Adding editors

If you try the "http://developer.yahoo.com/yui/examples/datatable/dt_cellediting.html">example
you might notice that if you try to edit the cells in the amount column
you are allowed to enter anything at all. It is when you click the ‘Ok’
button that the validateNumber function rejects the entry
and leaves the cell contents unmodified but, until you do so, you can

quite happily fill the textbox with all sorts of nonsense. I prefer my
editor to let me know when I am doing something wrong instead of waiting
until I thoroughly made a fool of myself. Fortunately, we are not limited
to the available editors.

Let’s take the standard TextboxCellEditor method and make an
RegExpCellEditor one where you can specify a regular
expression to validate the entry. Actually, we will handle two separate
regular expressions, one to be used while the value is being entered and
might not be yet complete. This will accept or reject keystrokes as they
are being pressed. The other regular expression is that for the final
value and it will highlight the entry by adding a className to the entry
box so the graphics designer can choose how to signal the user that the
value is not yet complete. For example to validate a US Social Security
Number, this is how we would specify the editor in the column definition:

editor:new YAHOO.widget.RegExpCellEditor({
    regExp:'^\\d{0,3}-?\\d{0,2}-?\\d{0,4}$',
    finalRegExp:'^\\d{3}-\\d{2}-\\d{4}$',
    failedRegExpClassName:'warning'
})

While the regExp property is full of conditionals so that it
won’t mind while the values are being entered, the
finalRegExp has no conditionals whatsoever, you have to put
3 digits, a dash, 2 digits, another dash and the final 4 digits and the
whole entry from beginning (^) to end ($) is validated. While the entry
doesn’t match finalRegExp it will be shown with whatever CSS
styles the style called .warning says, on the other hand, if
what you enter doesn’t match the regExp pattern, the
character will simply be rejected so, in this case, nothing but digits
and dashes can go, and those in the right places. The editor does not
replace the normal validator which is an all-or-nothing
validation once you have pressed Ok button, the editor does not prevent
an invalid value from being accepted. A validator function
might have to verify check-digits, check consistency with other values in
the same record or a number of other things and the
asyncSubmitter has the final say from the server itself. The
RegExpCellEditor only gives feedback on the proper format of
the entry, nothing more.

YAHOO.widget.RegExpCellEditor = function (oConfigs) {
    this._sId = "yui-regexptextboxceditor" + YAHOO.widget.BaseCellEditor._nCount++;
    oConfigs = oConfigs || {};
    oConfigs.type = 'regexptextbox';
    YAHOO.widget.RegExpCellEditor.superclass.constructor.call(this, oConfigs); 
    
};

First, the constructor for the cell editor. We first assemble an Id which
the cell editor will use, based on the type of editor (so it makes it
easy to find in the debugger) and a unique global index. We make sure the
oConfigs is an object and add a type property
to it. Then we call the constructor of the object we will inherit from, a
regular textbox editor, all the previous being a preparation for that
superclass constructor.

Next, as can be seen below, we extend the closest type of editor, the
TextboxCellEditor which we will use as the model for our own
editor. In fact, we will only add validation for the keys being typed
into the textbox. The YAHOO.lang.extend function accepts a
third argument which is an object containing the properties and methods
we mean to add or override. We add the three properties for our editor
and override the render method. This method normally draws
the actual HTML elements that will take the user input. We won’t actually
draw anything different so the first thing we do is to call the same
method of the superclass. What we are interested in is in setting up the
listeners for the keystrokes.

YAHOO.lang.extend(YAHOO.widget.RegExpCellEditor, YAHOO.widget.TextboxCellEditor, {
    regExp: null,
    finalRegExp: null,
    failedRegExpClassName : '',
    
    render: function () {
        YAHOO.widget.RegExpCellEditor.superclass.render.call(this);

        if (this.regExp && YAHOO.lang.isString(this.regExp)) { 
            this.regExp = new RegExp(this.regExp); 
        }
        if (this.finalRegExp && YAHOO.lang.isString(this.finalRegExp)) {
            this.finalRegExp = new RegExp(this.finalRegExp); 
        }

        YAHOO.util.Event.on(this.textbox,'keypress', function(ev) {
            if (YAHOO.lang.isNull(this.regExp)) { return; }
            var textbox = this.textbox;
            if (YAHOO.env.ua.gecko > 0 && ev.keyCode) { 
                return;
            }
            var ch = ev.keyCode || ev.charCode, 
                val = textbox.value, 
                start, 
                end; 
            if (document.selection && document.selection.createRange) {
                //undocumented IE trick to get the selection box.
                start = Math.abs(document.selection.createRange().moveStart("character", -1000000));
                end = Math.abs(document.selection.createRange().moveEnd("character", -1000000)); 
            } else {
                start = textbox.selectionStart;
                end = textbox.selectionEnd;
            }
            val = val.substr(0,start) + String.fromCharCode(ch) + val.substr(end);
            if (!this.regExp.test(val)) {
                YAHOO.util.Event.stopEvent(ev);
            }
        },this,true);
        YAHOO.util.Event.on(this.textbox,'keyup',function(ev) {
            if (YAHOO.lang.isNull(this.finalRegExp)) { return; }
            if (this.finalRegExp.test(this.textbox.value)) {
                YAHOO.util.Dom.removeClass(this.textbox,this.failedRegExpClassName);
            } else {
                YAHOO.util.Dom.addClass(this.textbox,this.failedRegExpClassName);
            }
        },this,true);
    }

});
// Just to copy static members, not really needed.
YAHOO.lang.augmentObject(YAHOO.widget.RegExpCellEditor, YAHOO.widget.TextboxCellEditor);

The listeners for the keystrokes are somewhat complicated and in the end
say little about the DataTable itself. The problem is that, while the
keyup listener allows the data to be read directly from the
input element, the character just entered can’t be rejected. The only way
to reject a character is to listen to the keypress event,
which can be cancelled. The problem there is that since the character has
not yet been inserted into the textbox, this process needs to be
emulated. Thus, we need to find out where the insertion point is to get
the character would go and that varies in between browsers. Once the
would-be entry is assembled, it is compared against the regular
expression and the character accepted (the default) or rejected via
stopEvent. We do use the keyup event to compare
against the finalRegExp and set or remove the given
className accordingly.

Extending the standard objects

The new version of the DataTable has brought many new classes. The
DataSource has been broken into several specific classes, the
ScrollableDataTable is now a separate thing from the simple DataTable,
the Paginator is not distributed along the DataTable and the editors are
now true objects. Extending them has become a little more complicated.

Why would you trouble yourself with extending them? If you have adopted
some sort of standard envelope for data transmission as has been
suggested, you might want to avoid specifying the
resultsList every time since it is always the same and, if
you override either doBeforeParseData or
doBeforeCallback you might want that to be taken care of as
well. And it you later change from, say, JSON to XML, you change your own
DataSource object and the applications hardly needs to learn about it.

To start with, the DataSource object cannot be extended
because it is not really a true object. What seems to be the constructor
is actually a factory which, depending on the
responseType declared returns instances of the specific
sub-classes derived from DataSourceBase so, extending
DataSource will, in the end, fail. When you need to extend a
DataSource, you have to extend the specific class you will
be using XHRDataSource, LocalDataSource or any
such sub-class.

If you extend the DataTable it will fail if you set the
scrollable configuration attribute. The regular DataTable
can be extended but when the scrollable is set it acts as a
factory for a ScrollableDataTable and will fail just as
extending DataSource or any such factory fails. Both the
plain DataSource object and the DataTable when
producing a scrollable data table are only entry points to provide
backward compatibility.

As for the Paginator object, the only difference now is that
it has to be explicitly loaded so it is more of a dependency issue.

The architecture of the cell editors has changed in important ways but
migrating the prior versions to 2.6 is not much of a headache as we have
already seen and the old style still works. Extending them is quite
something else. Actually, in prior versions what you did is changing the
plug-ins at the end of the core editor code, now you do really extend the
editors since they are real objects. Examples of such migrations from the
old plug-ins to the new objects can be found in the "http://www.satyam.com.ar/yui/#DataGrid">DataGrid and "http://www.satyam.com.ar/yui/#invoice">Invoice examples in my YUI
examples, both available for 2.5 and 2.6 (in fact, the
RegExpCellEditor above was taken from the later example).

Conclusion

In this and the "http://yuiblog.com/blog/2008/10/15/datatable-260-part-one/">previous
article
, we have seen how to create a DataTable, how to
make changes to the DataTable, how to report those changes
to the server and how, once accepted by the server, show those changes to
the user. We have always maintained the database server as the primary
source of our data, ensuring the DataTable does not show any
changes until they have been confirmed by the server. We have also seen
how to validate data input both at the row entry level and on a
field-by-field basis.

Many of these techniques have been put together in the following example,
which is available for download.

30 Comments

  1. Great.
    But I have question about how to refresh DataTable after reset new values for rows using setData method.
    Now it turns out new value in DataTable don’t appear in DataTable unless you click the cell of DataTable.
    Could you please help with it?
    Thanks.

  2. awhite

    setData only affects the underlying record, it will not refresh the UI. It is meant to be so, you are free to change many fields in several records before doing the refresh. In general, operations done at the Record level will not propagate to the DataTable which sits higher up in the object hierarchy. Changes done at the DataTable though methods updateCell or updateRow will immediately affect the DataTable and all underlying objects.

    Clicking on a cell to bring the editor makes the editor read the value store in the Record, not that in the screen so that when you save it back again, the new value will become visible. Sorting on any column will also make the DataTable re-read all the values from the Recordset.

  3. if I add a row dynamically, will the sorting still work, or would I need to refresh the page?

  4. Omer

    When you add a row you specify the index position where it will be inserted. The DataTable will not fit it where it should according to sort order, it will fit it where you told it to.

  5. Hi!I need do
    something like drop-down list from a table row.Like a tree. As the
    tree view but the tree nodes are rows of the table. i mean hide and
    show some rows of the table by some event(button press or row select).
    There are some ideas how to do it?

  6. Hi Daniel,

    Great article. Is there a way to check if a record already exists on the table prior to adding it?

    Thanks,
    Alex

  7. Lex

    The DataTable cannot know what would be a duplicate record. For some tables, duplicates are perfectly fine, for others, a single primary key column might be the the record identifier and it might not even be shown, for others the same record means all fields the same. For other tables, a duplicate record is perfectly fine. What each application considers a duplicate is up to the application, the DataTable cannot handle all the possible options.

  8. I see what you mean. For the application that I am trying to prototype duplicates are not good, I thought getRecordIndex would be a way of at least finding if a record already exists, but it only compares the unique id of the record within the datatable, so that didn’t help. Is there a quick way to find something by key?

    Thanks Daniel,
    Alex

  9. hey satyam(as in yui),

    This article is really nice.I have used inline editor custom validator.The asyncSubmitter it worked
    perfectly.

    Thanks man,
    ravindra

  10. Hi Satyam, another great article as always. However I think your practice of encoding the status inside the response body is questionable. By doing that you immediately lose the benefit of being able to determine the request status without caring whether the message is JSON encoded, XML encoded, etc.. HTTP status codes are actually intended to be used at the “application level” as you call it. For example, the 4xx range is specifically for client errors. If you make good use of them then your code will already be wired to gracefully handle other external errors such as 5xx server errors from your web server.

    Cheers,

    Patrick

  11. Hi i have 2 tables and i have to send both to the server. Its posible ? to send the datasource in format JSON, this tables are dinamic and i add rows like a ticket detail. I hope that you help me.

    Sorry, my english its very bad.

  12. Patrick,

    I admit that your suggestion of using the HTTP status codes is correct in principle. The problem is that the failure codes end up calling the ‘failure’ callback function, which receives very limited information. Compare the arguments received by the success and failure callbacks: http://developer.yahoo.com/yui/connection/#success

    On the server side, I use PHP and I trap all internal errors and process them through a function ajaxErrorHandler that formats them in proper JSON format. You could still receive that information in the o.statusText argument, but it is limited in length. And what if it was in XML format? You would not have it as an XML document, as you would with o.responseXML but as a string. Why do I care for such long error reports? PHP provides error messages as an array. SQL errors, for one, are reported by the PHP interpreter as such in one entry and then the error coming from the SQL engine is appended as a separate entry. The second entry, the actual SQL error is the one I want to see.

    Then, there is the problem that while HTTP docs enumerate some of the errors, it does not reserve a ‘user defined error codes’ that will never be used by them nor it says what all the intermediaries in the process, proxy servers, gateways and finally browsers, should do with them. There is nothing preventing such intermediary from plain quitting when it receives any code above 400 so that your application never knows about that.

  13. Miguel

    Your question will be handled more effectively in the Developers forum at: http://tech.groups.yahoo.com/group/ydn-javascript/

    There are more than eleven thousand subscribers there. Very few people will ever see your question here.

  14. Excellent article! It’s helped me tremendously in building my understanding of the DataTable.

    If I might suggest – if you use the ‘envelope’ type response you recommend, the delete function example you provided won’t delete the row after getting the response from the server. I figured out how to do it based on one of your later examples though by changing:

    success: function (o) {
    if (o.responseText == 'Ok') {
    this.deleteRow(target);

    to:

    success: function (o) {
    var r = YAHOO.lang.JSON.parse(o.responseText);
    if(r.replyCode == 200) {
    this.deleteRow(target);

  15. Hi Satyam, I want to reload data in tne yahoo table, but I can’t do it.
    For examle: I use html table as source. My html table sometimes changes (if i do one action), and I want that yahoo table does reload data (without reload view, because I want that my dragged and resized columns not moved back when I am reloading data).
    Sorry, my english is very bad.
    Thanks,
    Denis

  16. Denis

    A few comments above yours you will see the reference to the discussion group where your question will be answer far more effectively.

  17. The RegExpCellEditor could be validated if the validator method is overridden with the following:

    validator : function(newData, oldData, editor) {
    if (editor.finalRegExp.test(newData)) {
    return newData;
    }
    return undefined;
    },

  18. tib,

    Indeed, it can be validated so or by using an asyncSubmitter. However, such validation on its own will not produce the visual effect. The validator is not called until the input is submitted and the purpose was to give the user an on-going indication of the state of his entry while it is being entered.

    The validator can do more extensive tests than just compliance with a regular expression, for example, the validator can check the range of a date while the regular expression can only check that what is entered seems to be a date, and the asyncSubmitter would certainly go well beyond that, none of these options are excluded.

  19. Amrish Singh said:
    July 21, 2009 at 5:04 am

    Can i change the look and feel of textbox in datatable by extending TextBoxCellEditor. Instead of yui textbox i need HTML Textbox. If yes than please let me know how can i do with example..

  20. How can I add my database to this datatable in .NET?

    I have a datatable object of .Net, how can I add it to Yui datatable?

  21. @Amrish @Mehmet – A better place to ask these types of support questions would be the YUI forum at http://tech.groups.yahoo.com/group/ydn-javascript/. It’s a lively community of YUI developers who will likely be able to give you some advice. You can also look to see if your question has already been answered.

    Cheers,
    Jenny

  22. hii,
    I am planning to make a datatable to show data which refreshes every 5 seconds.The datatable will be resizable, sortable, column rearrangable and also with row selection.
    I am facing trouble with data updation and sortable feature.On refreshing the user defined sort resets to default.I want the sort to be maintained even after refreshing.

    Kindly please help me.
    Thankyou

  23. Hi,

    I have created a YUI datatable, and when i click the cell the editor gets opened.

    I want the editor that is getting opened to fit within the size of the cell, so that i looks like how we edit a cell in Excel.

    Can u please help me out in this.

  24. @Venkatesh

    A better place to ask this type of support question would be the YUI forum at http://tech.groups.yahoo.com/group/ydn-javascript/. It’s a lively community of YUI developers who will likely be able to give you some advice.

  25. Hola!
    Estoy usando la data table YAHOO.example.InlineCellEditing y tengo un una function para formater las celdas y poder agregarles un input type hidden pero mi problema es que no se como crearle un identificar unico osea un name unico para despues poder hacer sumit de todo el contenido y poder hacer update de la informacion. El codigo para el format es el sigiuiente:

    // Custom formatter for “Semana” column
    var formatSemana = function(elCell, oRecord, oColumn, oData) {
    elCell.innerHTML = oData + “”;
    };

    espero pueda ayudarme
    atte.

  26. Gelmin

    Just as the comment prior to yours says, this is not the place for support question, there is a forum for that.

  27. [...] foremost experts on YUI’s powerful DataTable Control, and he’s the author of important tutorials that have helped tens of thousands of developers make full use of DataTable. He’s also known [...]

  28. Hi satyam,
    Thanks for nice article.i have a question below
    I have to use datatable in my current assignment and edit few columns.But whenever i edit the column,validation should happen and on basis of the validation result some other column values should change.But values should not be stored at database.When submit happens then all the values from datatable should get saved at database. I guess i can’t use asyncrequest method just because it passes all the values as querystring which might be very lengthy. Appreciate any suggestions to acheive this.

  29. As many of the comments above say, there is a support forum and this is not it.