I am using jqGrid (inlineNav
) with data from azure service and interested in learning about inline editing and error handling with azure mobile service table.
Please share thoughts.
Code update 1: Code update based on Oleg's suggested way of using ondblClickRow
, Enter and Escape machanism
$("#list4").jqGrid({
url: myTableURL,
datatype: "json",
height: "auto",
colNames: ['RowNo', 'RouteId', 'Area'],
colModel: [
{ name: 'id', width: 70, editable: false },
{ name: 'RouteId', width: 70 },
{ name: 'Area', width: 150 }}
],
cmTemplate: { editable: true, editrules: { required: true} },
rowList: [10, 20, 30],
rowNum: 10,
sortname: "id",
prmNames: { search: null, nd: null },
ondblClickRow: function (rowid) {
var $self = $(this);
$self.jqGrid("editRow", rowid, {
mtype: "PATCH",
keys: true,
url: myTableURL + "/" +
$.jgrid.stripPref($self.jqGrid("getGridParam", "idPrefix"), rowid)
});
},
ajaxGridOptions: {
contentType: "application/json",
headers: {
"X-ZUMO-APPLICATION": "myKey"
}
},
serializeGridData: function (postData) {
if (postData.sidx) {
return {
$top: postData.rows,
$skip: (parseInt(postData.page, 10) - 1) * postData.rows,
$orderby: postData.sidx + " " + postData.sord,
$inlinecount: "allpages"
};
} else {
return {
$top: postData.rows,
$skip: (parseInt(postData.page, 10) - 1) * postData.rows,
$inlinecount: "allpages"
};
}
},
serializeRowData: function (postData) {
var dataToSend = $.extend(true, {}, postData);
if (dataToSend.hasOwnProperty("oper")) {
delete dataToSend.oper;
}
if (dataToSend.hasOwnProperty("id")) {
delete dataToSend.id;
}
return JSON.stringify(dataToSend);
},
beforeProcessing: function (data, textStatus, jqXHR) {
var rows = parseInt($(this).jqGrid("getGridParam", "rowNum"), 10);
data.total = Math.ceil(data.count / rows);
},
jsonReader: {
repeatitems: false,
root: "results",
records: "count"
},
loadError: function (jqXHR, textStatus, errorThrown) {
alert('HTTP status code: ' + jqXHR.status + '\n' +
'textStatus: ' + textStatus + '\n' +
'errorThrown: ' + errorThrown);
alert('HTTP message body (jqXHR.responseText): ' + '\n' + jqXHR.responseText);
},
pager: '#pager1',
viewrecords: true,
caption: "Schedule Data",
gridview: true,
autoencode: true
});
Combined code of inline editing and server side paging :
var $grid = $("#list4"),
azureHeaders = { "X-ZUMO-APPLICATION": "mykey" },
myTableURL = "https://mohit.azure-mobile.net/tables/Schedules",
inlineNavParams = {
save: false, // we want to add Save button manually. So we needn't no standard button
edit: true, add: true, del: true,
editParams: { mtype: "PATCH" },
addParams: {
addRowParams: {
//mtype: "POST", // default value
aftersavefunc: function (rowid, response) {
var rowData = $.parseJSON(response.responseText),
newId = rowData.id,
$self = $(this),
idPrefix = $self.jqGrid("getGridParam", "idPrefix", newId),
selrow = $self.jqGrid("getGridParam", "selrow", newId),
selArrayRow = $self.jqGrid("getGridParam", "selarrrow", newId),
oldId = $.jgrid.stripPref(idPrefix, rowid),
dataIndex = $self.jqGrid("getGridParam", "_index", newId),
i;
// update id in the _index
if (dataIndex != null && dataIndex[oldId] !== undefined) {
dataIndex[newId] = dataIndex[oldId];
delete dataIndex[oldId];
}
// update id attribute in <tr>
$("#" + $.jgrid.jqID(rowid)).attr("id", idPrefix + newId);
// update id of selected row
if (selrow === rowid) {
$self.jqGrid("setGridParam", { selrow: idPrefix + newId });
}
// update id in selarrrow array
// in case of usage multiselect:true option
if ($.isArray(selArrayRow)) {
i = $.inArray(rowid, selArrayRow);
if (i >= 0) {
selArrayRow[i] = idPrefix + newId;
}
}
// the next line is required if we use ajaxRowOptions: { async: true }
$self.jqGrid("showAddEditButtons");
}
}
}
};
// set common options which we want to use in inline editing
$.extend(true, $.jgrid.inlineEdit, {
keys: true,
afterrestorefunc: function () {
$(this).jqGrid("showAddEditButtons");
},
aftersavefunc: function () {
$(this).jqGrid("showAddEditButtons");
},
});
$grid.jqGrid({
colNames: ['RouteId', 'Area'],
colModel: [
{ name: 'RouteId', index: 'RouteId', width: 70 },
{ name: 'Area', index: 'Area', width: 150 }
],
cmTemplate: { editable: true, editrules: { required: true} },
// the parameters below are needed to load the grid data from the server
// we use loadonce: true option below. One can use server side pading instead.
// see http://stackoverflow.com/a/15979809/315935 for the changes
url: myTableURL,
rownumbers: true,
datatype: "json",
rowNum: 10,
rowList: [10, 20, 30],
prmNames: {search: null, nd: null, sort: null, rows: null},
ajaxGridOptions: { contentType: "application/json", headers: azureHeaders },
// jsonReader: {
// repeatitems: false,
// root: function (obj) { return obj; }
// },
jsonReader: {
repeatitems: false,
root: "results",
records: "count"
},
loadError: function (jqXHR, textStatus, errorThrown) {
alert('HTTP status code: ' + jqXHR.status + '\n' +
'textStatus: ' + textStatus + '\n' +
'errorThrown: ' + errorThrown);
alert('HTTP message body (jqXHR.responseText): ' + '\n' + jqXHR.responseText);
},
gridview: true,
autoencode: true,
height: "auto",
// we implement additionally inline editing on double-click.
// it's optional step in case of usage inlineNav
ondblClickRow: function (rowid) {
var $self = $(this);
$self.jqGrid("editRow", rowid, {
mtype: "PATCH",
keys: true,
url: myTableURL + "/" +
$.jgrid.stripPref($self.jqGrid("getGridParam", "idPrefix"), rowid)
});
},
// next options are important for inline editing
ajaxRowOptions: { contentType: "application/json", headers: azureHeaders },
editurl: myTableURL,
serializeRowData: function (postData) {
var dataToSend = $.extend(true, {}, postData);
if (dataToSend.hasOwnProperty("oper")) {
delete dataToSend.oper;
}
if (dataToSend.hasOwnProperty("id")) {
delete dataToSend.id;
}
return JSON.stringify(dataToSend);
},
serializeGridData: function (postData) {
if (postData.sidx) {
return {
$top: postData.rows,
$skip: (parseInt(postData.page, 10) - 1) * postData.rows,
$orderby: postData.sidx + " " + postData.sord,
$inlinecount: "allpages"
};
} else {
return {
$top: postData.rows,
$skip: (parseInt(postData.page, 10) - 1) * postData.rows,
$inlinecount: "allpages"
};
}
},
beforeProcessing: function (data, textStatus, jqXHR) {
var rows = parseInt($(this).jqGrid("getGridParam", "rowNum"), 10);
data.total = Math.ceil(data.count/rows);
},
viewrecords: true,
rownumbers: true,
height: "auto",
pager: "#pager1",
caption: "Windows Azure Mobile Services REST API"
}).jqGrid("navGrid", "#pager1", { edit: false, add: false, del: false, search: false });
$grid.jqGrid("inlineNav", "#pager1", inlineNavParams);
$grid.jqGrid("navButtonAdd", "#pager1", {
caption: $.jgrid.nav.savetext || "",
title: $.jgrid.nav.savetitle || "Save row",
buttonicon: "ui-icon-disk",
id: $grid[0].id + "_ilsave",
onClickButton: function () {
var $self = $(this),
gridIdSelector = $.jgrid.jqID(this.id),
savedRow = $self.jqGrid("getGridParam", "savedRow"),
prmNames = $self.jqGrid("getGridParam", "prmNames"),
editUrl = $self.jqGrid("getGridParam", "editurl"),
rowid = savedRow != null ? savedRow[0].id : "",
id = $.jgrid.stripPref($self.jqGrid("getGridParam", "idPrefix"), rowid),
tmpParams = {};
if (rowid != null) {
if ($("#" + $.jgrid.jqID(rowid), "#" + gridIdSelector).hasClass("jqgrid-new-row")) {
if (!inlineNavParams.addParams.addRowParams.extraparam) {
inlineNavParams.addParams.addRowParams.extraparam = {};
}
inlineNavParams.addParams.addRowParams.extraparam[prmNames.oper] = prmNames.addoper;
tmpParams = inlineNavParams.addParams.addRowParams;
} else {
if (!inlineNavParams.editParams.extraparam) {
inlineNavParams.editParams.extraparam = {};
}
inlineNavParams.editParams.extraparam[prmNames.oper] = prmNames.editoper;
inlineNavParams.editParams.url = editUrl + "/" + id;
tmpParams = inlineNavParams.editParams;
}
if ($self.jqGrid("saveRow", rowid, tmpParams)) {
$self.jqGrid("showAddEditButtons");
}
} else {
$.jgrid.viewModal("#alertmod", {gbox: "#gbox_" + gridIdSelector, jqm: true});
$("#jqg_alrt").focus();
}
}
});
$("#" + $grid[0].id + "_ilsave").addClass("ui-state-disabled");
Code update 3 :
var $grid = $("#list4");
var myTableURL = 'https://mohit.azure-mobile.net/tables/Schedules';
var azureHeaders = { "X-ZUMO-APPLICATION": ", mykey" };
$grid.jqGrid({
url: myTableURL,
editurl: myTableURL,
datatype: "json",
height: "auto",
colNames: ['RowNo', 'RouteId', 'Area', 'BusStop', 'Seater', 'Lat', 'Long', 'Timing', 'FromTo', 'KeyPoint'],
colModel: [
{ name: 'id', index: 'id', width: 70, editable: false },
{ name: 'RouteId', index: 'RouteId', width: 70 }
],
cmTemplate: { editable: true, editrules: { required: true} },
rowList: [10, 20, 30],
rowNum: 10,
sortname: "id",
prmNames: { search: null, nd: null},
ondblClickRow: function (rowid) {
var $self = $(this);
$self.jqGrid("editRow", rowid, {
mtype: "PATCH",
keys: true,
url: myTableURL + "/" + $.jgrid.stripPref($self.jqGrid("getGridParam", "idPrefix"), rowid)
});
},
ajaxGridOptions: { contentType: "application/json", headers: azureHeaders },
ajaxRowOptions: { contentType: "application/json", headers: azureHeaders },
serializeGridData: function (postData) {
if (postData.sidx) {
return {
$top: postData.rows,
$skip: (parseInt(postData.page, 10) - 1) * postData.rows,
$orderby: postData.sidx + " " + postData.sord,
$inlinecount: "allpages"
};
}
else {
return {
$top: postData.rows,
$skip: (parseInt(postData.page, 10) - 1) * postData.rows,
$inlinecount: "allpages"
};
}
},
serializeRowData: function (postData) {
var dataToSend = $.extend(true, {}, postData);
if (dataToSend.hasOwnProperty("oper")) {
delete dataToSend.oper;
}
if (dataToSend.hasOwnProperty("id")) {
delete dataToSend.id;
}
return JSON.stringify(dataToSend);
},
beforeProcessing: function (data, textStatus, jqXHR) {
var rows = parseInt($(this).jqGrid("getGridParam", "rowNum"), 10);
data.total = Math.ceil(data.count / rows);
},
jsonReader: {
repeatitems: false,
root: "results",
records: "count"
},
loadError: function (jqXHR, textStatus, errorThrown) {
alert('HTTP status code: ' + jqXHR.status + '\n' +
'textStatus: ' + textStatus + '\n' +
'errorThrown: ' + errorThrown);
alert('HTTP message body (jqXHR.responseText): ' + '\n' + jqXHR.responseText);
},
pager: '#pager1',
viewrecords: true,
caption: "Bus Schedule Data",
gridview: true,
autoencode: true
});
inlineNavParams = {
save: false, // we want to add Save button manually. So we needn't no standard button
edit: true, add: true, del: true,
editParams: { mtype: "PATCH" },
addParams: {
addRowParams: {
aftersavefunc: function (rowid, response) {
var rowData = $.parseJSON(response.responseText),
newId = rowData.id,
$self = $(this),
idPrefix = $self.jqGrid("getGridParam", "idPrefix", newId),
selrow = $self.jqGrid("getGridParam", "selrow", newId),
selArrayRow = $self.jqGrid("getGridParam", "selarrrow", newId),
oldId = $.jgrid.stripPref(idPrefix, rowid),
dataIndex = $self.jqGrid("getGridParam", "_index", newId),
i;
if (dataIndex != null && dataIndex[oldId] !== undefined) {
dataIndex[newId] = dataIndex[oldId];
delete dataIndex[oldId];
}
$("#" + $.jgrid.jqID(rowid)).attr("id", idPrefix + newId);
if (selrow === rowid) {
$self.jqGrid("setGridParam", { selrow: idPrefix + newId });
}
if ($.isArray(selArrayRow)) {
i = $.inArray(rowid, selArrayRow);
if (i >= 0) {
selArrayRow[i] = idPrefix + newId;
}
}
$self.jqGrid("showAddEditButtons");
}
}
}
};
$grid.jqGrid("navGrid", "#pager1", { edit: false, add: false, del: false, search: false });
$grid.jqGrid("inlineNav", "#pager1", inlineNavParams);
$grid.jqGrid("navButtonAdd", "#pager1", {
caption: $.jgrid.nav.savetext || "",
title: $.jgrid.nav.savetitle || "Save row",
buttonicon: "ui-icon-disk",
id: $grid[0].id + "_ilsave",
onClickButton: function () {
var $self = $(this),
gridIdSelector = $.jgrid.jqID(this.id),
savedRow = $self.jqGrid("getGridParam", "savedRow"),
prmNames = $self.jqGrid("getGridParam", "prmNames"),
editUrl = $self.jqGrid("getGridParam", "editurl"),
rowid = savedRow != null ? savedRow[0].id : "",
id = $.jgrid.stripPref($self.jqGrid("getGridParam", "idPrefix"), rowid),
tmpParams = {};
if (rowid != null) {
if ($("#" + $.jgrid.jqID(rowid), "#" + gridIdSelector).hasClass("jqgrid-new-row")) {
if (!inlineNavParams.addParams.addRowParams.extraparam) {
inlineNavParams.addParams.addRowParams.extraparam = {};
}
inlineNavParams.addParams.addRowParams.extraparam[prmNames.oper] = prmNames.addoper;
tmpParams = inlineNavParams.addParams.addRowParams;
} else {
if (!inlineNavParams.editParams.extraparam) {
inlineNavParams.editParams.extraparam = {};
}
inlineNavParams.editParams.extraparam[prmNames.oper] = prmNames.editoper;
inlineNavParams.editParams.url = editUrl + "/" + id;
tmpParams = inlineNavParams.editParams;
}
if ($self.jqGrid("saveRow", rowid, tmpParams)) {
$self.jqGrid("showAddEditButtons");
}
} else {
$.jgrid.viewModal("#alertmod", {gbox: "#gbox_" + gridIdSelector, jqm: true});
$("#jqg_alrt").focus();
}
}
});
$.extend(true, $.jgrid.inlineEdit, {
keys: true,
afterrestorefunc: function () {
$(this).jqGrid("showAddEditButtons");
},
aftersavefunc: function () {
$(this).jqGrid("showAddEditButtons");
},
});
Inline editing mode of jqGrid provide three base methods needed for implementing of editing: editRow, restoreRow and saveRow. The method addRow add empty row and then uses internally
editRow
for start editing. If one usekeys: true
option ofeditRow
then one don't need to callsaveRow
explicitly.editRow
do it internally if the user press Enter key in the editing field. The user can use Esc key to cancel editing.editRow
calls internallyrestoreRow
in the case instead ofsaveRow
.jqGrid introduced later
formatter: "actions"
, addRow and inlineNav which simplify a little the usage of inline editing if one needs to have some buttons for starting editing and saving the data. The most problems of usage offormatter: "actions"
and inlineNav are in the correct usage of parameters and because the methods provide you less control on usage of parameters. AdditionallyinlineNav
had many bugs which are fixed only in version 4.4.5.Sorry, for so long common text, but I want just explain why I want answer on your question first without usage of
inlineNav
and then provide solution which use the method.The most simple way to use inline editing for edit existing rows is the following. One starts just
editRow
inside ofondblClickRow
. The user can press key Enter to save the editing row on the server or press the key Esc to discard the changes. The corresponding code will be about the following:(To make the code easier I removed any error handling during loading of data or saving of editing results)
The above example works. The user can view the data, select rows, make local paging etc. On the other side the user can double-click on the row to edit it. It's important to understand that design inline editing allows editing multiple lines at the same side. The user can make double-click on one row, make some modifications, then make double-click on another row, makes some other modifications. Finally the user can finish editing of every row either by pressing on Enter or Esc key to save or to discard the current changes of the line.
At the beginning of editing of every row we set
url
option ofeditRow
which will be associated with the row of grid. If the user press Enter key the methodeditRow
calls internallysaveRow
with the same parameters.After you understand how inline editing work I can explain how all works in case of usage
inlineNav
. If you examine the code ofinlineNav
(see here) then you will see that it uses mostly the method navButtonAdd which add custom button to navigator bar. Inside ofonClickButton
callback it callsaddRow
,editRow
,saveRow
orrestoreRow
. The version 4.4.5 fixes many bugs ininlineNav
(see here, here, here, here and here), but it still don't solves not all existing problems. For example if you permit to edit multiple rows at the same time (if you use currently undocumented optionrestoreAfterSelect: false
ofinlineNav
) then because of usage$t.p.savedRow[0].id
expression to get the rowid the code of jqGrid can use wrong rowid for saving or discarding of row. So you should not use optionrestoreAfterSelect: false
in the current version of jqGrid.The main problem of
inlineNav
in my opinion is that one use not the same options for saving or discarding which one has during initializing of editing of rows. I mean thatinlineNav
callssaveRow
orrestoreRow
*not with the same options which was used for call ofeditRow
. If one changes for example theurl
property ofeditRow
so that RESTfullurl
withid
of row will be used the call ofsaveRow
will be not made with the same options if the user clicks on "Save" button.Moreover there are exist no callback which can be used to modify the current options (to modify
url
mostly) if the user click onsaveRow
. NetherinlineNav
norsaveRow
have currently (in jqGrid 4.4.5 or lower) such callbacks.The only way to solve the problem which I see is:
restoreAfterSelect: false
optionssave: false
option of inlineNavinlineNav
and modify theurl
option of manually before one callssaveRow
. In other words one should re-implement "Save" button ofinlineNav
.An example of the corresponding implementation you can find below. I used
loadonce: true
option. If one have large table and prefer server side paging then one will need make changes of some parameters correspond with my previous answer on your question. Additionally I removed error handling to simplify the code a little:How you can see the most complex is the implementation of
aftersavefunc
callback of theaddRowParams
parameter. I plan to post later my suggestion to trirand which extends the code of inline editing, but simplify the code ofaftersavefunc
callback so that it could be justAll other things should jqGrid do internally if the type of the value returned by
aftersavefunc
is not "undefined".