How can I test a trigger function in GAS? - debugging

Google Apps Script supports Triggers, that pass Events to trigger functions. Unfortunately, the development environment will let you test functions with no parameter passing, so you cannot simulate an event that way. If you try, you get an error like:
ReferenceError: 'e' is not defined.
Or
TypeError: Cannot read property *...* from undefined
(where e is undefined)
One could treat the event like an optional parameter, and insert a default value into the trigger function using any of the techniques from Is there a better way to do optional function parameters in JavaScript?. But that introduces a risk that a lazy programmer (hands up if that's you!) will leave that code behind, with unintended side effects.
Surely there are better ways?

You can write a test function that passes a simulated event to your trigger function. Here's an example that tests an onEdit() trigger function. It passes an event object with all the information described for "Spreadsheet Edit Events" in Understanding Events.
To use it, set your breakpoint in your target onEdit function, select function test_onEdit and hit Debug.
/**
* Test function for onEdit. Passes an event object to simulate an edit to
* a cell in a spreadsheet.
*
* Check for updates: https://stackoverflow.com/a/16089067/1677912
*
* See https://developers.google.com/apps-script/guides/triggers/events#google_sheets_events
*/
function test_onEdit() {
onEdit({
user : Session.getActiveUser().getEmail(),
source : SpreadsheetApp.getActiveSpreadsheet(),
range : SpreadsheetApp.getActiveSpreadsheet().getActiveCell(),
value : SpreadsheetApp.getActiveSpreadsheet().getActiveCell().getValue(),
authMode : "LIMITED"
});
}
If you're curious, this was written to test the onEdit function for Google Spreadsheet conditional on three cells.
Here's a test function for Spreadsheet Form Submission events. It builds its simulated event by reading form submission data. This was originally written for Getting TypeError in onFormSubmit trigger?.
/**
* Test function for Spreadsheet Form Submit trigger functions.
* Loops through content of sheet, creating simulated Form Submit Events.
*
* Check for updates: https://stackoverflow.com/a/16089067/1677912
*
* See https://developers.google.com/apps-script/guides/triggers/events#google_sheets_events
*/
function test_onFormSubmit() {
var dataRange = SpreadsheetApp.getActiveSheet().getDataRange();
var data = dataRange.getValues();
var headers = data[0];
// Start at row 1, skipping headers in row 0
for (var row=1; row < data.length; row++) {
var e = {};
e.values = data[row].filter(Boolean); // filter: https://stackoverflow.com/a/19888749
e.range = dataRange.offset(row,0,1,data[0].length);
e.namedValues = {};
// Loop through headers to create namedValues object
// NOTE: all namedValues are arrays.
for (var col=0; col<headers.length; col++) {
e.namedValues[headers[col]] = [data[row][col]];
}
// Pass the simulated event to onFormSubmit
onFormSubmit(e);
}
}
Tips
When simulating events, take care to match the documented event objects as close as possible.
If you wish to validate the documentation, you can log the received event from your trigger function.
Logger.log( JSON.stringify( e , null, 2 ) );
In Spreadsheet form submission events:
all namedValues values are arrays.
Timestamps are Strings, and their format will be localized to the Form's locale. If read from a spreadsheet with default formatting*, they are Date objects. If your trigger function relies on the string format of the timestamp (which is a Bad Idea), take care to ensure you simulate the value appropriately.
If you've got columns in your spreadsheet that are not in your form, the technique in this script will simulate an "event" with those additional values included, which is not what you'll receive from a form submission.
As reported in Issue 4335, the values array skips over blank answers (in "new Forms" + "new Sheets"). The filter(Boolean) method is used to simulate this behavior.
*A cell formatted "plain text" will preserve the date as a string, and is not a Good Idea.

Update 2020-2021:
You don't need to use any kind of mocks events as suggested in the previous answers.
As said in the question, If you directly "run" the function in the script editor, Errors like
TypeError: Cannot read property ... from undefined
are thrown. These are not the real errors. This error is only because you ran the function without a event. If your function isn't behaving as expected, You need to figure out the actual error:
To test a trigger function,
Trigger the corresponding event manually: i.e., To test onEdit, edit a cell in sheet; To test onFormSubmit, submit a dummy form response; To test doGet, navigate your browser to the published webapp /exec url.
If there are any errors, it is logged to stackdriver. To view those logs,
In Script editor > Execution icon on the left bar(Legacy editor: View > Executions).
Alternatively, Click here > Click the project you're interested in > Click "Executions" icon on the left bar(the 4th one)
You'll find a list of executions in the executions page. Make sure to clear out any filters like "Ran as:Me" on the top left to show all executions. Click the execution you're interested in, it'll show the error that caused the trigger to fail in red.
Note: Sometimes, The logs are not visible due to bugs. This is true especially in case of webapp being run by anonymous users. In such cases, It is recommended to Switch Default Google cloud project to a standard Google cloud project and use View> Stackdriver logging directly. See here for more information.
For further debugging, You can use edit the code to add console.log(/*object you're interested in*/) after any line you're interested in to see details of that object. It is highly recommended that you stringify the object you're looking for: console.log(JSON.stringify(e)) as the log viewer has idiosyncrasies. After adding console.log(), repeat from Step 1. Repeat this cycle until you've narrowed down the problem.
Congrats! You've successfully figured out the problem and crossed the first obstacle.

2017 Update:
Debug the Event objects with Stackdriver Logging for Google Apps Script. From the menu bar in the script editor, goto:
View > Stackdriver Logging to view or stream the logs.
console.log() will write DEBUG level messages
Example onEdit():
function onEdit (e) {
var debug_e = {
authMode: e.authMode,
range: e.range.getA1Notation(),
source: e.source.getId(),
user: e.user,
value: e.value,
oldValue: e. oldValue
}
console.log({message: 'onEdit() Event Object', eventObject: debug_e});
}
Example onFormSubmit():
function onFormSubmit (e) {
var debug_e = {
authMode: e.authMode,
namedValues: e.namedValues,
range: e.range.getA1Notation(),
value: e.value
}
console.log({message: 'onFormSubmit() Event Object', eventObject: debug_e});
}
Example onChange():
function onChange (e) {
var debug_e = {
authMode: e.authMode,
changeType: changeType,
user: e.user
}
console.log({message: 'onChange() Event Object', eventObject: debug_e});
}
Then check the logs in the Stackdriver UI labeled as the message string to see the output

As an addition to the method mentioned above (Update 2020) in point 4.:
Here is a small routine which I use to trace triggered code and that has saved me a lot of time already. Also I have two windows open: One with the stackdriver (executions), and one with the code (which mostly resides in a library), so I can easily spot the culprit.
/**
*
* like Logger.log %s in text is replaced by subsequent (stringified) elements in array A
* #param {string | object} text %s in text is replaced by elements of A[], if text is not a string, it is stringified and A is ignored
* #param {object[]} A array of objects to insert in text, replaces %s
* #returns {string} text with objects from A inserted
*/
function Stringify(text, A) {
var i = 0 ;
return (typeof text == 'string') ?
text.replace(
/%s/g,
function(m) {
if( i >= A.length) return m ;
var a = A[i++] ;
return (typeof a == 'string') ? a : JSON.stringify(a) ;
} )
: (typeof text == 'object') ? JSON.stringify(text) : text ;
}
/* use Logger (or console) to display text and variables. */
function T(text) {
Logger.log.apply(Logger, arguments) ;
var Content = Stringify( text, Array.prototype.slice.call(arguments,1) ) ;
return Content ;
}
/**** EXAMPLE OF USE ***/
function onSubmitForm(e) {
T("responses:\n%s" , e.response.getItemResponses().map(r => r.getResponse()) ;
}

Related

Retries until they pass or not pass possible with each()

I have a page with a grid view and the possibility to filter on elements. If i ex. do the following the test will fail as the grid view hasn´t updated when filter is applied.
cy.get('[data-cy="elements"]').each((element)=> {
expect(element.text()).to.equal('something)
});
Currently i have a custom command that wait for a loading indicator to disappear but i would love to see if there is a better solution like you can do when you have a single element: cy.get('[data-cy="elements"]').should('have.text, 'something');
I´ve seen that you could do something like this but then i have to check each element using eq which isn´t that beautiful:
cy.get('[data-cy="elements"]').should((elements)=> {
expect(elements.eq(0).text()).to.equal('something');
expect(elements.eq(1).text()).to.equal('something');
expect(elements.eq(2).text()).to.equal('something');
}}
use this:
cy.get('[data-cy="elements"]').should((elements)=> {
for(var i = 0; i < elements.length; i++) {
expect(elements.eq(i).text()).to.equal('something');
}
}
each is not suitable here because as the docu ( https://docs.cypress.io/api/commands/each.html#Assertions ) also states, it is not repeatable. So it will fail on the first failing assertion.
So use should with a callback function to take advantage of the repeat feature of cypress.

how to create a new record in form datasource from x++

In form Journal Voucher (AR>Journal> PaymentJournal> clicking buttonLines). I want to create a new record from x++ code.
I have seen few methods in the form viz create(), initvalue(), ledgerJournalEngine_custPayment... etc which are called when we press ctrl+n . How we could use these methods through x++ code to create a record using standard functionality.
plz help.
Before you elaborated, I thought you were trying to create your own custom form extending the journal functionality. If you're just trying to create a tool, you can just create a new Settlement Using Cust Group button. In the clicked event, call your transaction marking form or whatever you do to get the transactions you want to use. Then put something like this in it:
void clicked()
{
;
element.lock();
super();
// Put your code here to call the open transaction editing code
// CREATE THIS CUSTOM METHOD on C\LedgerJournalEngine_CustPayment\settleTransCustGroup
ledgerJournalEngine.settleTransCustGroup(ledgerJournalTable);
ledgerJournalTrans_ds.active();
ledgerJournalTrans_ds.reread();
ledgerJournalTrans_ds.executeQuery();
//recalculate balances
ledgerJournalEngine.newJournalActive(ledgerJournalTable, true);
element.firstField();
element.unLock();
}
Then in the new method you created, which I named settleTransCustGroup, you can loop over your records in the testLedgerJournalSpecTrans modeling off of something similar to this (custom method created on the engine class):
void settleTransCustGroup(LedgerJournalTable _ledgerJournalTable)
{
LedgerJournalTrans ledgerJournalTrans;
;
// Turn this stuff into a loop and default whatever else you need
ledgerJournalTrans.clear();
ledgerJournalTrans.initValue();
ledgerJournalTrans.AccountNum = '100003';
ledgerJournalTrans.AmountCurCredit = 10;
this.initValue(ledgerJournalTrans);
ledgerJournalTrans.insert();
this.write(ledgerJournalTrans);
ledgerJournalTrans.clear();
ledgerJournalTrans.initValue();
ledgerJournalTrans.AccountNum = '100005';
ledgerJournalTrans.AmountCurCredit = 15;
this.initValue(ledgerJournalTrans);
ledgerJournalTrans.insert();
this.write(ledgerJournalTrans);
}
Generally, your X++ code would look something like this:
static void InsertRecord(Args _args)
{
LedgerJournalTrans ledgerJournalTrans;
;
ledgerJournalTrans.AccountNum = "10000";
ledgerJournalTrans.AmountCurCredit = 50.64;
ledgerJournalTrans.AccountType = LedgerJournalACType::Ledger;
ledgerJournalTrans.insert();
}
You can replace the fields and values as needed. If any fields are missing, the error will display in the infolog (for example, if you were to run the above, you will get a "Currency code must be specified" error), so be sure all required fields are addressed.
In most cases, you can also call ledgerJournalTrans.initValue(); before assigning your values to pre-populate the record with default AX values. I believe this will be the same as what you see when you use Ctrl + N on the form. In the above example, doing so will cause the Currency Code to be filled in, and the record to be saved correctly (at least on our system).
There is no magical way of calling standard funcionality out of the frameworks quoted here on other comments. For each Ledger Type (Accounting, Inventory, Orders, Payments, ...), the way of creating and initializing lines is different and you have to work on this specific way if you want the journal to post properly.
There are a lot of examples on google of X++ code that inserts journal transactions for almost every type of them. It's not easy, but at least it's always almost the same code and it can be easilly reused.

jQuery Fullcalendar - How to update all changed events together instead of one by one?

I am using the FullCalendar jQuery plugin: http://arshaw.com/fullcalendar/
I am also using the example where you can drag external events onto the calendar: http://arshaw.com/js/fullcalendar-1.5.2/demos/external-dragging.html
Right now, I have an event click function as follows:
eventClick: function(event) {
$.ajax({
type: "POST",
url: "ajax-schedule.php",
data: 'id=' + event.id + '&start=' + event.start + '&end=' + event.end,
success: function(data){
alert('done!');
}
});
}
This posts to a file "ajax-schedule.php" where the data is inserted into the mysql database.
I would like to create a link that when clicked will take all of the new/changed events and post the data as shown above, instead of one-by-one.
Something like:
Update Schedule
The "updateSchedule" function would then post all the data.
Looks like the solution may involve the "clientEvents" method: http://arshaw.com/fullcalendar/docs/event_data/clientEvents/
... but I'm sort of lost here.
Any ideas as to how to do this?
You can create an array to store all the events:
var arrayOfEvents = [];
$('#calendar').fullCalendar({
...
drop: function(date) {
...
// retrieve the dropped element's stored Event Object
var originalEventObject = $(this).data('eventObject');
// we need to copy it, so that multiple events don't have a reference to the same object
var copiedEventObject = $.extend({}, originalEventObject);
// Push the event into the array
arrayOfEvents.push(copiedEventObject);
...
},
...
)};
function updateSchedule()
{
var data = "numOfEvents=" + arrayOfEvents.length;
// You can get all events out of the array here
for (var i = 0 ; i < arrayOfEvents.length ; i++) {
var event = arrayOfEvents[i];
data += "&id" + i + "=" + event.id
+ "&start" + i + "=" + event.start
+ "&end" + i + "=" + event.end;
}
// Make your ajax post here
$.ajax({
type: "POST",
url: "ajax-schedule.php",
data: data,
success: function(response){
alert('done!');
}
});
}
So on server-side, your code can get "numOfEvents" and just run a for loop from 0 to numOfEvents to get all events out.
This is something I am planning to implement in future when I get to tweaking performance of my project. My general idea would be something like this:
create handler that stores every change made in fullcalendar, so new events, events updates (drag&drop, resize, title/description/color/whatever is needed), event deletions. I guess the best way would be to create "class" that will be part of fullcalendar itself, tweakable by options of cource, if it wont be part of fc you would need to call it in every state changing function.
This handler stores array of events as well as it provides some basic methods to add fc changes into queue. My idea is to make it event-based, so in array there is member defined by event id which has information about every update made on this event. The way I imagine this to work is not exactly like every event has its own array of updates which server will run sequentially. I think of making this in way, that handler will be set to be saving data every 15/30... seconds (will be set by user) or on user call (pressing button for example). In time between two saves, queue will be populated in way where all updates will be merged into one global change (for example, if you move one event in calendar 5 times, resize 3 times and change title 5 times but at the end its gonna be the very same as it was at the last save, there will be nothing send to server for saving because in reality, no change was made. Or if you do the same and then delete event, its senseless to save all changes and then delete event, instead of that handler will send only delete command as this command is not affected by any other previously done. But if you move event for example two days in future and then 1 day back, it will calculate it was actually moved only by one day forward so there wont be any unnecessary data posted to server).
Eventhought it would be the best to implement it directly to fullCalendar plugin, it also can be standalone class/plugin which could be associated with any kind of application which makes a lot of changes on some set of datas and requires communication to be highly efficient to maximize speed (so user wont be bothered by slow updates/saves). It can be tweaked even more by recognizing exactly which fields (I use fc basically as google calendar, I can change color, desription, title and many more fields, but It would be useless to send whole event as it is if for example only title is changed, no need to send fields that remained the same) in event had been updated and send only those, so there will absolutely no redundant data sent to server. I guess I would do this as every event would have its member in queue array (as I said before) and when new member for event is added to queue, it will store also current event data (which are for comparison only, wont be send to server) for further comparison with next updates (if there will be any).
Hope you didnt get lost and catch my drift. This is just my idea for usefull feature, but I dont see myself working on it this year, depends on school/job. Its not that hard to make it actually, at least not in way I imagine it to be, so there may be someone else who will do it before I even start :)

jqgrid: how send and receive row data keeping edit mode

jqGrid has employee name and employee id columns.
If employee name has changed, server validate method should called to validate name. Current row columns should be updated from data returned by this method.
If employee id has changed, server validate method should called to validate id.
Current row columns should be updated from data returned by this method.
Preferably jqGrid should stay in edit mode so that user has possibility to continue changing, accept or reject changes.
How to implement this in inline and form editing?
I'm thinking about following possibilites:
Possibility 1.
Use editrules with custom validator like
editrules = new
{
custom = true,
custom_func = function(value, colname) { ??? }
},
Issues: How to get data from all columns, make sync or async call and update columns with this call results.
Possibility 2.
Require user to press Enter key to save row.
Issues: how to find which column was changed and pass this column number to server.
How to update current row data from server response.
Possibility 3.
using blur as described in Oleg great answer in
jqgrid change cell value and stay in edit mode
Issues: blur does not fire if data is entered and enter is pressed immediately. How to apply blur in this case ?
In summary server sice calculation/validation should be dones as follows:
If column in changed and focus moved out or enter is pressed in changed column to save, server side sync or if not possible then async method should be called. Changed column name and current edited row values like in edit method are passed as parameters to this method.
This method returns new values for edited row. current edited row values should be replaced with values returned by that method.
Update
Oleg answer assumes that primary key is modified. This factor is not important. Here is new version of question without primary keys and other updates:
jqGrid has product barcode and product name columns.
If product name has changed, server validate method should called to validate name. Current row columns should be updated from data returned by this method.
If product barcode has changed, server validate method should called to validate product barcode.
Current row columns should be updated from data returned by this method.
jqGrid should stay in edit mode so that user has possibility to continue changing, accept or reject changes.
How to implement this in inline and form editing?
I'm thinking about following possibilites:
Possibility 1.
Use editrules with custom validator like
editrules = new
{
custom = true,
custom_func = function(value, colname) { ??? }
},
Issue: custom_func does not fire if input element loses focus. It is called before save for all elements. So it cannot used.
Possibility 2.
Require user to press Enter key to save row.
Issues: how to find which column was changed and pass this column number to server.
Save method should known column (name or barcode change order) and fill different columns. This looks not reasonable.
Possibility 3.
using blur:
colModel: [{"label":"ProductCode","name":"ProductCode","editoptions":{
"dataEvents":[
{"type":"focus","fn":function(e) { ischanged=false}},
{"type":"change","fn":function(e) {ischanged=true}},
{"type":"keydown","fn":function(e) {ischanged=true }},
{"type":"blur","fn":function(e) { if(ischanged) validate(e)} }
]},
To implement validate I found code from Oleg great answer in
jqgrid change cell value and stay in edit mode
Summary of requirement:
If column in changed and focus moved out or enter is pressed in changed column to save, server side sync or if not possible then async method should be called. Changed column name and current edited row values like in edit method are passed as parameters to this method.
This method returns new values for edited row. current edited row values should be replaced with values returned by that method.
Update2
This question is not about concurrency. This is single user and jqGrid issue. Updating means that single user changes product name or barcode and server shoudl provide additonal data (product id and/or name/barcode) is responce of this.
Update 4
I tried code below.
If user enters new code and presses Enter without moving to other row, blur does not occur and validation is not called.
How to dedect in jqGrid save method if cell is dirty or other idea how to force this code to run if enter is pressed to end edit without losing focus from changed foreign key cell ?
function validate(elem, column) {
ischanged = false;
var i, form, row;
var postData = { _column: column };
var colModel = $("#grid").jqGrid('getGridParam', 'colModel');
var formEdit = $(elem).is('.FormElement');
// todo: use jQuery serialize() ???
if (formEdit) {
form = $(elem).closest('form.FormGrid');
postData._rowid = $("#grid").jqGrid('getGridParam', 'selrow');
for (i = 0; i < colModel.length; i++)
eval('postData.' + colModel[i].name + '="' + $('#' + colModel[i].name + '.FormElement', form[0]).val() + '";');
}
else {
row = $(elem).closest('tr.jqgrow');
postData._rowid = row.attr('id');
for (i = 1; i < colModel.length; i++)
eval('postData.' + colModel[i].name + '="' + $('#' + postData._rowid + '_' + colModel[i].name).val() + '";');
}
$.ajax('Grid/Validate', {
data: postData,
async: false,
type: 'POST',
success: function (data, textStatus, jqXHR) {
for (i = 0; i < data.length; i++) {
if (formEdit)
$('#' + data[i].name + '.FormElement', form[0]).val(data[i].value);
else
$('#' + postData._rowid + '_' + data[i].name).val(data[i].value);
}
}
});
}
colModel is defined as:
{"name":"ProductBarCode",
"editoptions": {"dataEvents":
[{"type":"focus","fn":function(e) {ischanged=false}
},
{"type":"change","fn":function(e) {ischanged=true},
{"type":"keydown","fn":function(e) {if(realchangekey()) ischanged=true}
},{"type":"blur","fn":function(e) { if(ischanged) { validate( e.target,ProductBarCode')}}
}]},"editable":true}
It's one from the problems which is much easier to avoid as to eliminate. I have to remind you about my advises (in the comments to the answer) to use immutable primary key, so that is, will be never changed. The record of the database table can be destroyed, but no new record should have the id of ever deleted record.
On any concurrency control implementation it is important that the server will be first able to detect the concurrency problem. It can be that two (or more) users of your web application read the same information like the information about the employee. The information can be displayed in jqGrids for example. If you allow to change the employee id, than the first problem would be to detect concurrency error. Let us one user will change the employee id and another user will try to modify the same employee based on the previous loaded information. After the user submit the midification, the server application will just receive the "edit" request but will not find the corresponding record in the database. The server will have to sent error response without any detail. So the errorfunc of the editRow or the event handler errorTextFormat of the editGridRow should trigger "reloadGrid" reload the whole grid contain.
If you allow to edit the primary key, then I can imagine more dangerous situation as described before. It can be that another user not only change the id of the current editing row to another value, but one could change the id of one more record so, that its new id will be the same as the current editing id. In the case the request to save the row will overwrite another record.
To prevent such problems and to simplify the optimistic concurrency control one can add an additional column which represent any form of the timestamp in every table of the database which could be modified. I personally use Microsoft SQL Server and add I used to add the non-nullable column of the type rowversion (the same as the type timestamp in the previous version of the SQL Server). The value of the rowversion will be send to the jqGrid together with the data. The modification request which will be send to the server will contain the rowversion. If any data will be save in the database the corresponding value in the corresponding rowversion column will be automatically modified by the SQL database. In the way the server can very easy detect concurrency errors with the following code
CREATE PROCEDURE dbo.spEmployeesUpdate
-- #originalRowUpdateTimeStamp used for optimistic concurrency mechanism
-- it is the value which correspond the data used by the user as the source
#Id int,
#EmployeeName varchar(100),
#originalRowUpdateTimeStamp rowversion,
#NewRowUpdateTimeStamp rowversion OUTPUT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
-- ExecuteNonQuery() returns -1, but it is not an error
-- one should test #NewRowUpdateTimeStamp for DBNull
SET NOCOUNT ON;
UPDATE dbo.Employees
SET Name = #EmployeeName
WHERE Id=#Id AND RowUpdateTimeStamp=#originalRowUpdateTimeStamp;
-- get the new value of the RowUpdateTimeStamp (rowversion)
-- if the previous update took place
SET #NewRowUpdateTimeStamp = (SELECT RowUpdateTimeStamp
FROM dbo.Employees
WHERE ##ROWCOUNT > 0 AND Id=#Id)
END
You can verify in the code of the server application that the output parameter #NewRowUpdateTimeStamp will be set by the stored procedure dbo.spEmployeesUpdate. If it's not set the server application can throw DBConcurrencyException exception.
So in my opinion you should make modifications in the database and the servers application code to implement optimistic concurrency control. After that the server code should return response with HTTP error code in case of concurrency error. The errorfunc of the editRow or the event handler errorTextFormat of the editGridRow should reload the new values of the currently modified row. You can use either the more complex way or just reload the grid and continue the modification of the current row. In case of unchanged rowid you can easy find the new loaded row and to start it's editing after the grid reloading.
In the existing database you can use
ALTER TABLE dbo.Employees ADD NewId int IDENTITY NOT NULL
ALTER TABLE dbo.Employees ADD RowUpdateTimeStamp rowversion NOT NULL
ALTER TABLE dbo.Employees ADD CONSTRAINT UC_Employees_NewId UNIQUE NONCLUSTERED (NewId)
GO
Then you can use NewId instead of the id in the jqGrid or in any other place which you need. The NewId can coexist with your current primary key till you update other parts of your application to use more effective NewId.
UPDATED: I don't think that one really need to implement any complex error correction for the concurrency error. In the projects at my customers the data which are need be edited can not contain any long texts. So the simple message, which describe the reason why the current modifications could not be saved, is enough. The user can manually reload the full grid and verify the current contain of the row which he edited. One should not forget that any complex procedures can bring additional errors in the project, the implementation is complex, it extend the development budget and mostly the additional investment could never paid off.
If you do need implement automated refresh of the editing row I would never implement the cell validation in "on blur" event for example. Instead of that one can verify inside of errorfunc of the editRow or inside of the errorTextFormat event handler of the editGridRow that the server returns the concurrency error. In case of the concurrency error one can save the id of the current editing row in a variable which could be accessed inside of the loadComplete event handle. Then, after displaying of the error message, one can just reload the grid with respect of $('#list').trigger('reloadGrid',[{current:true}]) (see here). Inside of loadComplete event handle one can verify whether the variable of the aborted editing row is set. In the case one can call editRow or editGridRow and continue the editing of the string. I think that when the current row are changed another rows of the page could be also be changed. So reloading of the current page is better as reloading of the data only one current cell or one row of the grid.

Handling parameters from dynamic form for one-to-many relationships in grails

My main question here is dealing with the pramas map when having a one-to-many relationship managed within one dynamic form, as well as best practices for dealing with one-to-many when editing/updating a domain object through the dynamic form. The inputs for my questions are as follows.
I have managed to hack away a form that allows me to create the domain objects shown below in one Dynamic form, since there is no point in having a separate form for creating phone numbers and then assigning them to a contact, it makes sense to just create everything in one form in my application. I managed to implement something similar to what I have asked in my Previous Question (thanks for the people who helped out)
class Contact{
String firstName
String lastName
// ....
// some other properties
// ...
static hasMany = [phones:Phone]
static mapping = {
phones sort:"index", cascade: "all-delete-orphan"
}
}
class Phone{
int index
String number
String type
Contact contact
static belongsTo = [contact:Contact]
}
I basically managed to get the values from the 'params' map and parse them on my own and create the domain object and association manually. I.e. i did not use the same logic that is used in the default scaffolding, i.e.
Contact c = new Contact(params)
etc...., i just looped through all the params and hand crafted my domain objects and saved them and everything works out fine.
My controller has code blocks that look like this (this is stripped down, just to show a point)
//create the contact by handpicking params values
def cntct = new Contact()
cntct.firstName = params.firstName
cntct.lastName = params.lastName
//etc...
//get array of values for number,type
def numbers = params['phone.number']
def types = params['phone.type']
//loop through one of the arrays and create the phones
numbers.eachWithIndex(){ num, i ->
//create the phone domain object from
def phone = new Phone()
phone.number = num
phone.type = types[i]
phone.index = i
cntct.addToPhones(phone)
}
//save
My questions are as follows:
What is the best practice of handeling such a situation, would using Command objects work in this case, if yes where can i found more info about this, all the examples I have found during my search deal with one-to-one relationships, I couldn't find an example for one-to-many?
What is the best way to deal with the relatiohsips of the phones in this case, in terms of add/removing phones when editing the contact object. I mean the creation logic is simple since I have to always create new phones on save, but when dealing with updating a contact, the user might have removed a phone and/or editing an exiting one and/or added some new phones. Right now what I do is just delete all the phones a contact has and re-create them according to what was posted by the form, but I feel that's not the best way to do it, I also don't think looping over the existing ones and comparing with the posted values and doing a manual diff is the best way to do it either, is there a best practice on how to deal with this?
Thanks, hopefully the questions are clear.
[edit] Just for more information, phone information can be added and deleted dynamically using javascript (jquery) within the form [/edit]
disclaimer: i do not know if the following approach works when using grails. Let me know later.
See better way for dynamic forms. The author says:
To add LineItems I have some js that calculates the new index and adds that to the DOM. When deleting a LineItem i have to renumber all the indexes and it is what i would like to avoid
So what i do
I have a variable which stores the next index
var nextIndex = 0;
When the page is loaded, i perform a JavaScript function which calculates how many child The collection has and configure nextIndex variable. You can use JQuery or YUI, feel free.
Adding a child statically
I create a variable which store the template (Notice {index})
var child = "<div>"
+= "<div>"
+= "<label>Name</label>"
+= "<input type="text" name=\"childList[{index}].name\"/>"
+= "</div>"
+= "</div>"
When the user click on the Add child button, i replace {index} - by using regex - by the value stored in the nextIndex variable and increment by one. Then i add to the DOM
See also Add and Remove HTML elements dynamically with Javascript
Adding a child dinamically
Here you can see The Paolo Bergantino solution
By removing
But i think it is the issue grow up when deleting. No matter how many child you remove, does not touch on the nextIndex variable. See here
/**
* var nextIndex = 3;
*/
<input type="text" name="childList[0].name"/>
<input type="text" name="childList[1].name"/> // It will be removed
<input type="text" name="childList[2].name"/>
Suppose i remove childList1 What i do ??? Should i renumber all the indexes ???
On the server side i use AutoPopulatingList. Because childList1 has been removed, AutoPopulatingList handles it as null. So on the initialization i do
List<Child> childList = new AutoPopulatingList(new ElementFactory() {
public Object createElement(int index) throws ElementInstantiationException {
/**
* remove any null value added
*/
childList.removeAll(Collections.singletonList(null));
return new Child();
}
});
This way, my collection just contains two child (without any null value) and i do not need to renumber all the indexes on the client side
About adding/removing you can see this link where i show a scenario wich can gives you some insight.
See also Grails UI plugin
Thanks,
Your answer brought some insight for me to do a wider search and I actually found a great post that covers all the inputs in my question. This is just a reference for anyone reading this. I will write a blog entry on how I implemented my case soon, but this link should provide a good source of ino with a working exmaple.
http://www.2paths.com/2009/10/01/one-to-many-relationships-in-grails-forms/
Most of the time I use ajax to manage such problem.
So when the user clicks add new phone I get the template UI from the server for manageability purpose ( the UI just same GSP template that I use to edit, update the phone), so this way you are not mixing your UI with your js code, whenever you want to change the UI you have to deal only with our GSP code.
Then after getting the UI I add it to the page using jquery DOM manipulation. Then after filling the form when they hit add(save) the request is sent to the server via ajax and is persisted immediately.
When the user clicks edit phone the same UI template is loaded from the server filled with existing phone data, then clicking update will update the corresponding phone immediately via ajax, and same thing applies to delete operation.
But one day I got an additional scenario for the use case that says, "until I say save contact no phone shall be saved on the backend, also after adding phones to the contact on the ui if navigate away to another page and come back later to the contact page the phones I added before must be still there." ugh..
To do this I started using the Session, so the above operations I explained will act on the phone list object I stored on the session instead of the DB. This is simple perform all the operation on the phonesInSession but finally dont forget to do this(delete update):
phonesToBeDeleted = phonesInDB - phonesInSession
phonesToBeDeleted.each{
contact.removeFromPhones(it)
it.delete()
}
I know I dont have to put a lot of data in session but this is the only solution I got for my scenario.
If someone has got similar problem/solution please leave a comment.
First, in all your input fields names you add an #:
<input type="text" name="references[#].name"/>
Second, add call a function before submitting:
<g:form action="save" onsubmit="replaceAllWildCardsWithConsecutiveNumbers();">
Third, this is the code for the function that you call before submitting the form:
function replaceAllWildCardsWithConsecutiveNumbers(){
var inputs = $('form').find("[name*='#']");
var names = $.map(inputs, function(el) { return el.name });
var uniqueNames = unique(names);
for (index in uniqueNames) {
var uniqueName = uniqueNames[index];
replaceWildCardsWithConsecutiveNumbers("input", uniqueName);
replaceWildCardsWithConsecutiveNumbers("select", uniqueName);
}
}
function unique(array){
return array.filter(function(el, index, arr) {
return index === arr.indexOf(el);
});
}
function replaceWildCardsWithConsecutiveNumbers(inputName, name){
counter = 0;
$(inputName + "[name='" + name + "']").each(function (i, el) {
var curName = $(this).attr('name');
var newName = curName.replace("#", counter);
$(this).attr('name', newName);
counter += 1;
});
}
Basically, what the code for replaceAllWildCardsWithConsecutiveNumbers() does, is to create a list for all input (or select) elements whose name contains an #. Removes the duplicates. And then iterates over them replacing the # with a number.
This works great if you have a table and you are submitting the values to a command object's list when creating a domain class for the first time. If you are updating I guess you'll have to change the value of counter to something higher.
I hope this helps someone else since I was stuck on this issue for a while myself.

Resources