Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

Creating an In-Place Editing Component for Table Editing with jQuery


:P
On this page:

I spent a few hours today playing around with building a jQuery plug-in that provides editable behavior to text elements. The idea is that you can make any element editable and make changes to it which effectively changes the DOM content by overlaying a textbox/textarea and editing the content.

I initially started with this because I have a customer who needs to make changes to a few columns in a table. There are a already a few jQuery components out there that do some of this, but they aren't exactly what I needed and I figured this would be a good chance to put jQuery through its paces. As so many things I thought this would be quick and easy, but as I worked through it I found 'just one more thing' to add. And so goes the day - but it's been a fun excercise.

You can check out the end result of my first cut here:

http://www.west-wind.com/wconnect/webcontrols/EditableDataGrid.wcsx

Incidentally this is a West Wind Web Connection (FoxPro) sample rather than an ASP.NET one as I was working on updates to West Wind Web Connection at the time. However, that really shouldn't make any difference since the code at this point is all client based anyway and can be used with any web back end although there are some issues to consider with ASP.NET and gridviews/datagrids. I'll talk about that at the end.

There are a few things that this component does that I didn't see in others. Namely there's a bunch of state carried in each Editable object when it's edited so that you can make some smart choices of how to update data. For example, the sample only sends data to the server with Ajax calls when data has changed. Also the display indicates when a field has been updated. There's support for specifying individual columns in a table (via jQuery selectors) and rudimentary table navigation support so you can tab through fields as you enter data. There's also the ability to edit elements that contain hyperlinks (as the Company field) and maintain the hyperlink status after editing the text rather than replacing the link with plain text which is actually a common scenario.You can also specify the columns in a table that you want editable and the navigation features skip over non-edit fields.

So yeah I re-invented the wheel to some extent but there's a bunch of functionality I need beyond what I found and I thought it'd be a good excercise for me to use jQuery as part of more library oriented code  and what the heck it's Sunday (and now it's 3am after a day on the water <g>).

How it works

This sample basically uses the wwEditable client class - or more specifically the jQuery plugin that in turn calls on the wwEditable class - to make each of the columns in the table editable. You can either make the whole table editable (actually not a great idea if you have a pager since the pager is a TD tag that also becomes editable <s>) or pick columns individually.

The code to do this is as simple as this:

// *** make each column - based on css tag - editable
jQuery(".datecolumn,.companycolumn,.namecolumn")
      .makeEditable(ShowUpdateColumn,{ updatedColor:"green" } );

Well - as simple as that, except that in order for this to work each column has to be marked in some way so that you can find it using jQuery selectors. In this case I use a CssClass I assigned to each cell - one that doesn't actually exist - to tie the column name to the class. This serves two purposes: First jQuery can find the columns, but secondly it allows the code receiving a callback when editing is complete to know which column of data is being updated.

The makeEditable method can also be applied against any element that has text content other than table cells. So if you have a Div tag with just text you can edit it. For example if you have a text block you might do:

jQuery("#divMessage").makeEditable(updateText,{ textMode: "multiline"} );

Now clicking on the divMessage field will activate it for editing.

Alternately to make a whole table editable:

// *** Make a whole table editable
// *** Works but makes the pager also editable - undesirable here
jQuery("#gdDeveloperList").makeTableEditable(ShowUpdateColumn, { updatedColor: "green" } );

Here the root table is selected and made editable. Again, this basically makes *all* TD cells in the table editable and in the case of the sample above this would include the pager.

The jQuery plugin creates instances of the class for each matched element (or all table cells in the case of makeTableEditable) and hooks up an onclick handler that activates the edit mode.

If you want more control for an individual editable element you can directly create the wwEditable instance and manipulate it directly:

var ed = new wwEditable("#divMessage");

...
ed.edit();

Using wwEditable doesn't hook up a click handler and only when explicitly calling edit() is the textbox for editing created along with events that are trapped for losing focus. In effect, using wwEditable directly requires you to invoke the object explicitly. So to manually hook up to a click event of an element you could use:

<td class="nameColumn" onclick="new wwEditable(this,onnamechanged).edit()">some text value</td>

IOW, the raw instance is manually hooked up while the static jQuery plug-in methods create ready to use editable access including event handlers and batch hookup based on the selectors that are used to find matched elements.

When using the static jQuery methods onclick is hooked and the user should then provide a callback function that is invoked when focus leaves the edit area. The callback then receives the value and the editable object based on which the edited text either updates the original content or aborts.

Here's all of the user code that's used to handle the editing including AJAX callbacks (using wwWebMethodMethod callbacks):

<script type="text/javascript">        
 
jQuery("document").ready(function() {    
 
    // *** Make a whole table editable
    // *** Works but makes the pager also editable - undesirable here
    //jQuery("#gdDeveloperList").makeTableEditable(ShowUpdateColumn, { updatedColor: "lightgreen" } );
 
 
    // *** Better approach: make each column - based on css tag - editable
    jQuery(".datecolumn,.companycolumn,.namecolumn")
          .makeEditable(ShowUpdateColumn,{ updatedColor:"green" } );
} );
  
function ShowUpdateColumn(value, editable)
{            
   if (editable.origText == editable.enteredText)
   {
      showMessage(column + " not updated. Same value.");
      return false; // leave original
   }
   showMessage("updating on server...");
 
   // *** Figure out the column name by 'convention' of classname for element
   var column = editable.jcol.attr("class");       
 
   // *** Get the Row Pk from tr/Row's ID: gridname_Pk
   var pk = editable.jcol[0].parentNode.id.split("_")[1];               
 
   // *** Call the server callback method with wwWebCallbackMethod 
   // *** Proxy is the static var based on the control's ID.      
   Proxy.callMethod("UpdateField",[value,column,pk],updateField_Callback);
 
   // *** Create expando property with the editable class so we can
   // *** pick it up in the callback and complete the edit/abort cycle
   Proxy.lastEditable = editable;
 
    // *** Return null to keep from updating field
    // *** We'll update - or abort - in the callback
    return null;                    
}
function updateField_Callback(result, proxy)
{
    var editable = proxy.lastEditable;
 
    if (!result)
    {
       editable.abort();
       showMessage("Request aborted on the server.");
       return;
    }
 
    showMessage("Server Update:  " + result);
 
    editable.update();        
    editable.nextColumn();
}
</script>

The sample sends data to the server - the entered value, a pk (Retrieved from a custom ID scheme for the active row) and the column name gleamed from the CssClass applied. In West Wind Web Connection the row Id can be generated automatically. In ASP.NET getting the active row ID in client script is a bit more complicated - I haven't looked into how this can be done but I suspect it'll mean adding custom attributes of some sort.

The server example, sends the data to the server but it doesn't do anything with it - I didn't want to have you guys going nuts on the data here <g> so it just returns a confirmation that the data got there and displays that on the status line. Realistically the server code would need to take the pk land updated value and then update the database with the value.

Note that the edit callback receives both the value and the editable object. The editable object maintains internal values for the original text and html as well as the entered text so you can compare the two. This allows a bit more control over when and how to update the server.

There are a also a few niceties in the component. For example, notice that when you edit the Company, which is hyperlinked, the editor edits and updates the text and doesn't blow away the hyperlink. You can also tab around the table (forwards at least and backwards in the same row). When a field is updated a little tick mark with the updateColor is displayed in the upper right corner of the element to notify that the value has changed.

Here's the code for the wwEditable component and the jQuery plug-in functions and static instance. Note that the code depends on jQuery 1.2.

 
function wwEditable(column, callback, options) {
    var _I = this;
    var _Ctl = column;            
    if (typeof(column)=="string")
        _Ctl = jQuery("#" + column)[0];    
 
    // options
    this.callback = callback;
    this.extraData = null;   
    this.textMode = "text"
    this.updatedColor = null;
 
    // operationals
    this.origHtml = null;    
    this.origText = null;
    this.enteredText = null;            
    this.jcol = jQuery(_Ctl);    
    this.jedit = null;
    _Ctl.wwEditable = true;
 
    if (options)
        jQuery.extend(this,options);
 
    this.edit = function()
    {        
        if (callback)
          _I.callback = callback;
 
        var jctl = jQuery("#__editColumn");
        if (jctl.length > 0)
            return;
 
        _I.origHtml = _I.jcol.html();
        _I.origText = _I.jcol.text();
        if (_I.textMode == "text")       
            _I.jcol.html("<" +"form action='javascript:void(0);'><input id='__editColumn' value='" + _I.jcol.text() + "' style='width:" + _I.jcol.css("width") + "' /></form>");
        else
            _I.jcol.html("<" + "form action='javascript:void(0);'><textarea id='__editColumn' style='width:" + _I.jcol.css("width") + "'>" +  _I.jcol.text() + "</textarea></form>");
 
        var jctl = jQuery("#__editColumn");
        _I.jedit = jctl;        
        setTimeout( function() { jctl[0].focus() },50);
 
        jctl.keydown(_I._keyDownHandler);
        _I._bindBlur();
    }
    this.update = function() {           
        if (_I.jedit == null)
          return; 
 
        _I.jedit.remove();    
        _I.jedit = null;          
 
        if (_I.origHtml.toLowerCase().substr(0,2) == "<a") {
            _I.jcol.html(_I.origHtml);            
            var c = _I.jcol.find("a");
            _I.jcol.find("a").text(_I.enteredText);
        }
        else
            _I.jcol.text(_I.enteredText);        
 
        if (_I.updatedColor)
           _I.jcol.prepend("<div style='float: right;background:" + _I.updatedColor + ";height: 5px; width: 5px;'></div>");
 
        _I.origHtml = _I.jcol.html();
    }
    this.abort = function()
    {    
        _I.jedit.remove();        
        _I.jedit == null;
        _I.jcol.html(_I.origHtml);                    
    }
    this.nextColumn = function(jcols)
    {
          if (!jcols)
            jcols = _I.jcol.find("~td");
 
          if (jcols.length>0)
          {
              for(var x=0;x<jcols.length;x++) {
                var jitem = jQuery(jcols[x]);
                if (jitem[0].wwEditable) {
                    jitem.trigger("click");
                    break;
                }
              }
          }
          else{                
            var row = _I.jcol.parent().find("+tr");                    
            if (row.length > 0)
               _I.nextColumn(row.find(">td"));  
        }
    }
    this._updateHandler=function(event)
    {                            
        _I.enteredText = _I.jedit.val();    
        _I._bindBlur(true);
        var res = null;        
        if (_I.callback) {                               
           res = _I.callback(_I.enteredText,_I)
           if (res==false) _I.abort(); 
           if (res) _I.update();           
        }                
        _I._bindBlur();                          
    }
    this._bindBlur = function(unbind)
    {
        if (unbind)
            _I.jedit.unbind("blur");
        else
            _I.jedit.bind("blur",_I._updateHandler);
    }
    this._keyDownHandler = function(event)
    {                                    
        if (event.keyCode == 27)
            {_I.abort(); return;}
        if (event.keyCode == 13){ 
            if (_I.textMode!="multiline"){
                _I._updateHandler();
                _I.nextColumn();
                return false;
            } 
        }                      
        if (event.keyCode == 9 && event.shiftKey) {
            _I._updateHandler();                            
            var prev = _I.jcol[0].previousSibling;
            if (prev)
                   jQuery(prev).trigger("click");              
            return;            
        }
        if (event.keyCode == 9) {            
            _I._updateHandler();
            _I.nextColumn();                                                                                                       
        }    
    }
}
 
// Hook up as plug-in
// static object
jQuery.editable = 
{
    makeEditable:  function(selector, callback, options)
    {            
        var s = jQuery(selector);
        if (s.length < 1)
           return;
 
        s.each( function(index) {          
            var jitem = jQuery(this);        
            this.wwEditable = true;
            jitem.click(function(event) {new wwEditable(this,callback,options).edit(); return true; } );                    
        } );    
        return s;
    },
    makeTableEditable: function(selector,callback,options)
    {
        var s = jQuery(selector);
        if (s.length < 1)
           return;    
        this.makeEditable(s.find("td"),callback,options);    
        return s;
    }
}
 
// jQuery selector function extensions
jQuery.fn.makeEditable = function(callback,options)
{
    return jQuery.editable.makeEditable(this,callback,options);
}
jQuery.fn.makeTableEditable = function(callback,options)
{
   return jQuery.editable.makeTableEditable(this,callback,options);
}

Just a first cut

I'm just throwing this out today for some thoughts after working on this for the better part of the day. There are a still a number of things that need addressing. Navigation with keys needs a few tweaks.  The Shift-Tab navigation is not fully working and up and down keys would also be nice. There are also issues with the async callbacks. There's no good way to determine right now how the user navigated off the edit area and currently the code forces the focus to the next field (Tab behavior essentially) even if the user actually clicks somewhere else. Somehow the blur action needs to capture the source of the event.

I also couldn't figure out how to pre-select a textbox's text (ie. SelText like text selection on input fields). It'd be nice if you could select the complete text in a textbox. IE and FireFox handle the textbox cursor placement differently. FF puts it at the end, IE at the beginning. FF is easier but non-intuitive. Ideally I'd want to have the whole text selected so you can overwrite by default or press home to clear as you do in other applications. Ah the simple things you can't do in the browser (at least not easily - I spent an hour look at TextRanges but it didn't work right in FF).

Finally I haven't really looked at this in the ASP.NET scenario. In West Wind Web Connection I have the advantage of being in full control of the framework and so in order to make this work I was able to change the row rendering in the base grid control to render an id that can optionally contain a DataKey. I'm not sure if that's possible with ASP.NET at least not without overriding the base behavior of the gridview/datagrid/columns implementations. The difficulty lies in figuring out how to get the row and column information from the client to the server to make sure the server can actually update the data properly.

In addition, it's important to understand that updates aren't automatic. I have to play with this but it may be possible to somehow make this automatic, but I'm not sure that this is a good idea anyway - data needs to be validated first anyway so it may just be better to run a case statement to check and see which column is being updated, validating the input and then running the appropriate update code through business objects or raw SQL commands. While this may sound pretty tedious it's not too bad given that most grids won't have a ton of columns.

A few other thoughts might be to provide options to 'edit' in different ways. For example, allow a checkbox for a bool value (ie. textode="checkbox").

Another cool thing to do might be to combine this sort of plug-in with the masked input plug-in.

This component will be eventually merged into the West Wind Ajax Toolkit, but for now it's just a prototype to play with. Note that the component itself doesn't include any AJAX features so for server update code you'll need to use either jQuery, the WWATK or some other library that provides your server connectivity.

Download wwEditable.js JavaScript

Posted in jQuery  JavaScript  

The Voices of Reason


 

Marc Grabanski
January 28, 2008

# Creating an Live Editing Component for Table Editing with jQuery

Interesting, I have not seen this attempted in an ASP.NET control before.

Kevin Pirkl
January 28, 2008

# re: Creating an Live Editing Component for Table Editing with jQuery

Sweet work Rick.. Always love to see examples like this using jQuery coolness like this.. You Rock! I'm pouring over Chaffer & Swedberg's book right now and every time I use jQuery I learn something new. I like the learning jQuery site http://www.learningjquery.com/ and John Resig's blog http://ejohn.org/blog/

We have jQuery on over five thousand .Net content pages and teach our content creators how to use it. On the dev side we have jQuery integrated JSONP style services (.Net back-ended) that are used on all our pages for tagging, comments, feedback, voting, polling, email a friend and tons of other features integrated with .Net services. We lump these all the tools under a category called Common Services Framework (CSF) components (a term I heard coined at a Web 2.0 conference by a person from the O'Reilly team.) One JavaScript file and one HTML tag later and your in business.

Cheers

Kevin Pirkl

David Castro
February 11, 2008

# re: Creating an In-Place Editing Component for Table Editing with jQuery

Thanks for writing this up! I look forward to trying this out for a couple of clients of mine.

emin
February 01, 2010

# re: Creating an In-Place Editing Component for Table Editing with jQuery

Amazing tutorial. I'm new in jQuery, and trying to use this component (for educational purposes). I couldn't find where we make the call to server. How do we make such call? :)

Thanks in advance.

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024