Work continues on liteGrid (I believe that’s the name I’m going to stick with), and in fact so much has changed that I really don’t even know where to start when talking about it. The core has been changed around a bit (for the better), many new modules have been added, an AJAX data provider has been added… it’s been a busy couple of weeks. Today, I thought I would start with something fairly straight-forward: a new module that brings the power of jEditable into liteGrid.
If you recall my previous post, I basically wrote all the cell-editing script from scratch. It worked well enough and was extensible, but as I noted in my article, James Kolpack pointed out that I really did a lot more work than was necessary. So, in what little “spare time” I have, I implemented a module that achieves the same result (click-to-edit) using jEditable instead of custom script. This has several advantages. First, jEditable has a fairly rich set of editors already, and there are even 3rd party add-ons for additional editor types. Second, the goal of liteGrid isn’t editing, the goal is to provide a flexible, extensible, and lightweight grid. Maintaining my own editing library wasn’t going to help me achieve that goal. So, the old InlineEditingModule was thrown away, and JEditableModule has taken it’s place.
Let’s start with the high-level summary first:
function JEditableModule() { //Key codes. var enterKey = 13; var base = this; //Registers for events. base.initialize = function(liteGrid, options) { ... } //Attaches jEditable to editable columns. base.columnBound = function(event, column, tdElement) { ... } //Callback that is run whenenever a cell has been saved. This //stores the cell value in the underlying data item. base.saveCell = function(value, settings) { ... } //Callback that is run after a cell's value has been changed. base.afterSave = function(value, settings) { ... } } JEditableModule.defaultOptions = { placeholder: "", onblur: "submit", type: "text" };
There is the usual initialize function that all liteGrid modules must define. Next is an event handler that fires when columns are bound. Finally, there are two helpers: one that is responsible for actually updating the underlying dataItem when a value changes, and one that is called after a cell has been updated. There are also some default options that can be overridden by the liteGrid options (as we’ll see in a second).
The magic begins in the initialize function:
base.initialize = function(liteGrid, options) { base.liteGrid = liteGrid; base.options = options; //If jEditable isn't defined, we can't do anything. if (!$.editable) { console.log("Unable to initialize, can't find the jEditable plug-in."); return; } liteGrid.$el.bind("columnBound", base.columnBound); }
If the jEditable plug-in isn’t available, initialization is aborted, and an error is logged to Firebug. Otherwise, the module registers for columnBound events:
base.columnBound = function(event, column, tdElement) { //If the column isn't editable, or if we've already applied //jEditable, don't do anything. if (column.editable !== true || tdElement.hasClass("editable")) { return; } //Additional options are stored in the settings, making them available to callback functions. var options = $.extend({}, JEditableModule.defaultOptions, { callback: base.afterSave, column: column, tdElement: tdElement }); if (column.type) { //If the type isn't supported, alert the user. if (!$.editable.types[column.type]) { console.warn("Unable to find editor for type " + column.type + " in jEditable."); return; } options.type = column.type; } //Special-case: the built-in select editor requires additional properties that define the options. if (column.type == "select") { options.data = column.selectOptions; //This will end edit mode when the user presses enter. tdElement.keyup(function(event) { if (event.keyCode == enterKey) $("select", tdElement).blur(); }); } //Make the element editable. tdElement.editable(base.saveCell, options); tdElement.addClass("editable"); }
If the column isn’t editable, or if it has already been processed (as indicated by the marker class “editable”), nothing is done. Otherwise, an options object is built up that will be passed on to jEditable. If the column type doesn’t have a defined editor in jEditable, a warning is logged, and processing terminates. While jEditable likes to post changes to a URL via AJAX by default, it also supports a callback to handle the save, which is leveraged here with base.saveCell.
For “select” types, which render as dropdown lists, a couple of extra steps are required. First, the options for the select are copied from the column definition. I’m not completely satisfied with this approach as I do not like how jEditable requires you to specify your columns, but it works (for now). Second, the standard textbox editors persist their changes when the user hits the enter key. That doesn’t happen for dropdown lists, so a function is attached that triggers the blur event for the select, thereby triggering the value to persist.
The saveCell function is called by jEditable when the user has indicated that they want to persist a new value:
base.saveCell = function(value, settings) { var cell = $(settings.tdElement); //See if the value actually changed var dataItem = cell.parent().data("dataItem"); var currentValue = dataItem[settings.column.field]; //An event is raised so that interested parties can modify the //value prior to attempting to persist it. //TODO: ADD HOOKS FOR VALIDATION! var event = $.Event("valueChanged"); event.currentValue = currentValue; event.newValue = value; event.column = settings.column; base.liteGrid.$el.trigger(event); //Subscribers may have modified the new value value = event.newValue; //If the value hasn't changed, or if the value is still null/empty, don't do anything. if (currentValue == value || ((currentValue || null) == null && value == "")) { settings.valueChanged = false; } else { //Mark the cell as having been changed. This is used by the //callback handler. settings.valueChanged = true; cell.addClass("modified"); dataItem[settings.column.field] = value; cell.parent().data("dataItem", dataItem); } return value; }
The underlying data item is retrieved from the parent row so that the new value can be compared to the current value. An event is fired with column, new value, and current value. This allows interested parties to modify the value if they so choose. Ideally, validation could also be handled here, but I haven’t added that (yet). A simple check is performed to see if the new value has changed. If so, the cell is marked as changed, and the data item is updated.
After jEditable has saved the new value using the saveCell method, it calls the afterSave function:
base.afterSave = function(value, settings) { if (settings.valueChanged == true) { base.liteGrid.$el.trigger("columnBound", [settings.column, settings.tdElement]); } }
This callback looks to see if the value actually changed, and if so, raises the columnBound event.
And that’s it. It’s considerably simpler than the old method, and aside from a few rendering bugs with IE 8 that I haven’t ironed out yet, I don’t see any reason to use the old InlineEditModule instead of this new JEditableModule.
Crowd: “This is all well and good, but let’s see it in action!”
Yeah, I’m working on that. Once I get it migrated to Google Code, I’ll stand up some demos that people can play around with. Until then, you’ll just have to believe me when I tell you that it works.