On my current app we are using ajax heavily for updating forms, often with the response getting injected into an table or unordered list. The entire form is then posted for the action which will actually change state in the domain.
For example, say we have a 'NewItem' form (and have several properties):
<form action="myaction.castle">
<label for="newItem_Name">Name</label>
<input type="text" name="newItem.Name" id="newItem_Name"/>
<label for="newItem_Phone">Name</label>
<input type="text" name="newItem.Phone" id="newItem_Phone"/>
<!--10 other properties-->
<input type="submit">Save</input>
</form>
With the action on the controller:
public void myaction([DataBind("newItem")] ItemDTO item)
{
PropertyBag["item"] = item;
RenderView("ViewWithSophisticatedFormattingForItems");
}
This simply pushes back an item using the data provided and jquery appends the table with:
<tr><td><% output item.Name %></td></tr>
<tr style='display:none;'><td><% output Form.TextField("item.Name") %></td></tr>
The client will toggle between 'readonly' and 'edit' modes to change data inline.
So this would result in multiple rows of 'item.Name' and 'item.Phone', etc. and the DataBinder will choke if you try this:
public void UpdateToDomain([DataBind("item")] List<ItemDTO> items)
{
//do stuff in domain
}
As magical as MonoRail and her SmartDispatcherController is, she has to know which value belongs to which item in a collection. The lack of indices in the params will therefore cause arrays of 'Value's for a single ItemDTO instance.
My first thought was to do a custom implementation of IParameterBinder + DataBinder combo that parses out the params and tries to split them into multiple instances. This is too difficult though since not all fields may be present for an instance.
So to keep the items in a collection (like table rows or unordered lists) nicely indexed, I created a plugin that handles all the form elements for me. After ajax calls that load data into a table or ul, I can just call the plugin on the rows and all forms elements remain in their appropriate order. Similarly I can unindex forms for posting in the format the action expects. I can similarly just reorder indexes.
This just preserves the id and name convention used throughout monorail (with "_" and "[index]" delimiters respectively). It also changes the 'for' attribute on the label elements for fun.
This is chainable.
To use:
$("table.specimens > tbody tr.edit").indices("on"); //index all rows with class 'edit'
$("table.specimens > tbody tr.edit").indices("off"); //remove index on all rows with class '.edit'
$("table.specimens > tbody tr.edit").indices("ordered"); //ordered existing indices
$("table.specimens > tbody tr.edit").indices();//toggle indexed/nonindexed mode
//specify the parameterName prefix if you are indexing on a nested collection
$("table.specimens > tbody tr.edit").indices("on",{parameterName:'mainclass.nestedObjects'});
//customize whether a new row should increment the index (default is to skip increments on items with class 'repeat-index')
$("table.specimens > tbody tr.edit").indices("on",{isRepeatedIndex:function(){...});
And the plugin:
/***indices plugin for ordering form elements in a group
@mode (String) :
"on" (index elements if not already indexed),
"off" (unindex elements if already indexed),
"toggle" (toggle between "on" and "off" modes),
"ordered" (reorder indices of indexed items, or )
@options (hash) :
@parameterName (String) : the parameter name to index; if empty, uses the 'name' attribute up to the first '.'
@isRepeatedIndex (predicate[current item]) : return 'true' to repeat the index (for things like hidden rows, etc); default is a class name 'repeat-index'
***/
(function($) {
$.fn.indices = function(mode, options) {
mode = mode || "toggle";
var defaults = {
parameterName: "[a-zA-Z0-9]+",
isRepeatedIndex: function(item) { return $(item).is(".repeat-index"); }
};
var opt = $.extend(defaults, options);
var reIndexedName = new RegExp("^(" + opt.parameterName + ")\\[(\\d+)\\]\\.", "gi");
var reIndexedId = new RegExp("^(" + opt.parameterName.replace(".", "_") + ")_\\d+\\_", "gi");
var reNonIndexedName = new RegExp("^(" + opt.parameterName + ")\\.", "gi");
var reNonIndexedId = new RegExp("^(" + opt.parameterName.replace(".", "_") + ")_", "gi");
var changeStrategy = null;
var ndx = -1;
var unindexElement = function(el, ndx) {
if (el.name) {
$(el).attr("name", el.name.replace(reIndexedName, "$1."));
}
if (el.id) {
$(el).attr("id", el.id.replace(reIndexedId, "$1_"));
}
if ($(el).attr("for")) {
$(el).attr("for", $(el).attr("for").replace(reIndexedId, "$1_"));
}
};
var indexElement = function(el, ndx) {
if (el.name) {
$(el).attr("name", el.name.replace(reNonIndexedName, "$1[" + ndx + "]."));
}
if (el.id) {
$(el).attr("id", el.id.replace(reNonIndexedId, "$1_" + ndx + "_"));
}
if ($(el).attr("for")) {
$(el).attr("for", $(el).attr("for").replace(reNonIndexedId, "$1_" + ndx + "_"));
}
};
var reindexElement = function(el, ndx) {
if (el.name) {
$(el).attr("name", el.name.replace(reIndexedName, "$1[" + ndx + "]."));
}
if (el.id) {
$(el).attr("id", el.id.replace(reIndexedId, "$1_" + ndx + "_"));
}
if ($(el).attr("for")) {
$(el).attr("for", $(el).attr("for").replace(reIndexedId, "$1_" + ndx + "_"));
}
};
return this.each(function(idx, item) {
if (!opt.isRepeatedIndex(item)) {
ndx++;
}
$(this).find(":input,label").each(function() {
if (!changeStrategy) {
//evaluate only once per collection
if (!this.name && !this.id) {
//nothing to do here
return;
}
changeStrategy = function() { }; //default empty function
var isIndexed = this.name ? reIndexedName.test(this.name) : this.id ? reIndexedId.test(this.id) : false;
if (mode == "ordered" && !isIndexed) {
mode = "on"; //we already do ordered indexing so just use the 'on' mode
}
if (isIndexed) {
if (mode == "off" || mode == "toggle") {
changeStrategy = unindexElement;
} else if (mode == "ordered") {
changeStrategy = reindexElement
}
} else {
if (mode == "on" || mode == "toggle") {
changeStrategy = indexElement;
}
}
}
changeStrategy(this, ndx); //fire
});
});
};
I don't like having to scrub the data like this on the client side, but I really can't think of a way to keep parameters being sent valid while still updating data in small chunks without passing indexes back and forth. Any other approaches would be great to hear!