Hi I have some cells with custom rendering
when I update the all data, the deltaRowDataMode does not hnadle the change of my cutsom cell rendering. Other cells of the updated row are correctly updated.
How can I give a clue to ag grid to compare correctly this custom cell
I have just had the same issue and found a clue from the ag-grid documentation. In the Cell Renderer help doc it talks about the ICellRendererComp.refresh method:
// Mandatory - Get the cell to refresh. Return true if the refresh succeeded, otherwise return false.
// If you return false, the grid will remove the component from the DOM and create
// a new component in it's place with the new values.
refresh(params: ICellRendererParams): boolean;
and in the example further down:
// gets called whenever the user gets the cell to refresh
MyCellRenderer.prototype.refresh = function(params) {
// set value into cell again
this.eValue.innerHTML = params.valueFormatted ? params.valueFormatted : params.value;
// return true to tell the grid we refreshed successfully
return true;
};
I then implemented a refresh function on my CellRenderer as below without changing any of the cell contents:
statusCellRenderer.prototype.refresh = function (params) {
//ensure the status cell\directive refreshes when the grid data is refreshed using deltaRowDataMode
this.params = params;
return true;
};
So in my case, I am refreshing the rowData of the grid on a polling cycling and I didn't want the grid to keep losing the selected row. I set the deltaRowDataMode and getRowNodeId properties on the gridOptions and then implemented the refresh function to make the cell re-render on a refresh. The refresh also re-renders a directive in my cell.
Related
I'm trying to create a sidebar that will display cell values from the given row in which the user is currently on. For example, user is in cell B3. Side bar to show values from cells B1, B6 and B10.
I've tried several solutions, the latest being this one. Sadly, none of these work. Any ideas?
Additional information, 03/09/21:
I've created a sample spreadsheet with all the scripts.
The sidebar is now loading, but I cannot get it to update using the getRecord function. Keep getting the following error: NetworkError: Connection failure due to HTTP 502
Thanks
In your SidebarJavascript.html file, it doesn't look to me that you are passing the record to your callback. See if this helps:
function poll(interval) {
interval = interval || 1000;
setTimeout(function() {
google.script.run
.withSuccessHandler(showRecord) // <--replace this
.withSuccessHandler(function (record) { //<-- with this
showRecord(record);
})
.withFailureHandler(
function(msg, element) {
showStatus(msg, $('#button-bar'));
element.disabled = false;
})
.getRecord();
}, interval);
};
I don't know if this will make a difference.
I'm working on master/detail page - master records are in a Kendo drop down list and associated detail data is in a kendo grid.
The dd & grid are bound to remote data.
Updating existing grid rows is working fine.
When a new row is saved to the grid, I need to insert the id of the selected drop down item (master record id) and add it to the json data.
My problem is I don't know how to determine if the data being saved is a new record or an edit.
I'm getting this error: "Uncaught TypeError : Cannot read property 'isNew' of undefined"
Thanks for any guidance here.
$issuegrid
->addColumn($issueOwnerCol)
->addColumn($issueDescriptionCol)
->addColumn($issueDueDateCol)
->pageable(false) //this is the toolbar in the footer
->height(300)
->navigatable(true)
->editable(true)
->save('onSave')
->edit('onEdit')
**->saveChanges('onSaveChanges')**
->addToolbarItem($igridCreate)
->addToolbarItem($igridSave)
->addToolbarItem($igridCancel);
Here's the js function:
function **onSaveChanges**(e){
var grid = $("#issuesGrid").data("kendoGrid");
var URL ="/issues/updaterecord.json";
var ddl = $("#woDD").data("kendoDropDownList");
var v = ddl.value();
if (grid.dataSource.data.model.isNew()){
alert("New Record")
}
grid.dataSource.transport.options.update.url = URL;
grid.dataSource.sync();
}
Well, it's a workaround, but in my onSaveChanges function, I had to append the required id to the create url and then extract the it on the server. I would have preferred to just add a key/value pair to the json payload.
function onSaveChanges(e){
var grid = $("#issuesGrid").data("kendoGrid");
var ddl = $("#woDD").data("kendoDropDownList");
var woID = ddl.value();
var createURL ="/issues/addrecord.json?woID=" + woID;
grid.dataSource.transport.options.create.url = createURL;
//when updating an edited record, transport will use the default url defined in php code.
}
I would like to use Google Apps Script UiService to produce a multiple page user interface.
Here's what I've got so far:
function doGet(e)
{
var app=UiApp.createApplication();
var nameLabel=app.createLabel('Name:');
var button=app.createButton("next");//my button on clicking,trying to divert to other UI
var handler=app.createServerHandler("myclick");
button.addClickHandler(handler);
app.add(namelabel);
app.add(button);
return app;
}
function myClick(){
//on clicking the button it should call the other ui or other html page
is there any method for that.}
How can I do this?
You should look at How To Allow Users to Review Answers before Submiting Form?, which has an example that does this.
The idea is to create your UiApp with multiple Panels, then show or hide them in response to user actions, using setVisible(). (If you were using the HtmlService, you would enclose your "pages" in different <div>s, and change their display attributes. See toggle show/hide div with button?.)
The Best Practices also describes use of client-side handlers for responsiveness, so let's try that.
/**
* Very simple multiple page UiApp.
*
* This function defines two panels, which appear to the end user
* as separate web pages. Visibility of each panel is set to
* control what the user sees.
*/
function doGet() {
var app = UiApp.createApplication();
var page1 = app.createFlowPanel().setId('page1');
var page2 = app.createFlowPanel().setId('page2');
// Content for Page 1
page1.add(app.createLabel('Page 1'));
var page1Button = app.createButton('Next Page');
page1.add(page1Button);
// Create client handler to "change pages" in browser
var gotoPage2 = app.createClientHandler()
.forTargets(page1).setVisible(false)
.forTargets(page2).setVisible(true);
page1Button.addClickHandler(gotoPage2);
// Content for Page 2
page2.add(app.createLabel('Page 2'));
var page2Button = app.createButton('Previous Page');
page2.add(page2Button);
// Create client handler to "change pages" in browser
var gotoPage1 = app.createClientHandler()
.forTargets(page1).setVisible(true)
.forTargets(page2).setVisible(false);
page2Button.addClickHandler(gotoPage1);
app.add(page1);
app.add(page2);
// Set initial visibility
page1.setVisible(true);
page2.setVisible(false);
return app;
}
That works for changing the view of the UI. To extend this for general purposes, you would likely want to add server-side handlers to the same buttons to perform work, and update the contents of the panels as things progress.
Here is working code
that demonstrates a multiple page form, i.e. it does the initial doGet() and then lets you advance back and forth doing multiple doPost()'s. All this is done in a single getForm() function called by both the standard doGet() and the doPost() functions.
// Muliple page form using Google Apps Script
function doGet(eventInfo) {return GUI(eventInfo)};
function doPost(eventInfo) {return GUI(eventInfo)};
function GUI (eventInfo) {
var n = (eventInfo.parameter.state == void(0) ? 0 : parseInt(eventInfo.parameter.state));
var ui = ((n == 0)? UiApp.createApplication() : UiApp.getActiveApplication());
var Form;
switch(n){
case 0: {
Form = getForm(eventInfo,n); // Use identical forms for demo purpose only
} break;
case 1: {
Form = getForm(eventInfo,n); // In reality, each form would differ but...
} break;
default: {
Form = getForm(eventInfo,n) // each form must abide by (implement) the hidden state variable
} break;
}
return ui.add(Form);
};
function getForm(eventInfo,n) {
var ui = UiApp.getActiveApplication();
// Increment the ID stored in a hidden text-box
var state = ui.createTextBox().setId('state').setName('state').setValue(1+n).setVisible(true).setEnabled(false);
var H1 = ui.createHTML("<H1>Form "+n+"</H1>");
var H2 = ui.createHTML(
"<h2>"+(eventInfo.parameter.formId==void(0)?"":"Created by submission of form "+eventInfo.parameter.formId)+"</h2>");
// Add three submit buttons to go forward, backward and to validate the form
var Next = ui.createSubmitButton("Next").setEnabled(true).setVisible(true);
var Back = ui.createSubmitButton("Back").setEnabled(n>1).setVisible(true);
var Validate = ui.createSubmitButton("Validate").setEnabled(n>0).setVisible(true);
var Buttons = ui.createHorizontalPanel().add(Back).add(Validate).add(Next);
var Body = ui.createVerticalPanel().add(H1).add(H2).add(state).add(Buttons).add(getParameters(eventInfo));
var Form = ui.createFormPanel().setId((n>0?'doPost[':'doGet[')+n+']').add(Body);
// Add client handlers using setText() to adjust state prior to form submission
// NB: Use of the .setValue(val) and .setValue(val,bool) methods give runtime errors!
var onClickValidateHandler = ui.createClientHandler().forTargets(state).setText(''+(parseInt(n)));
var onClickBackHandler = ui.createClientHandler().forTargets(state).setText(''+(parseInt(n)-1));
Validate.addClickHandler(onClickValidateHandler);
Back.addClickHandler(onClickBackHandler);
// Add a client handler executed prior to form submission
var onFormSubmit = ui.createClientHandler()
.forTargets(state).setEnabled(true) // Enable so value gets included in post parameters
.forTargets(Body).setStyleAttribute("backgroundColor","#EEE");
Form.addSubmitHandler(onFormSubmit);
return Form;
}
function getParameters(eventInfo) {
var ui = UiApp.getActiveApplication();
var panel = ui.createVerticalPanel().add(ui.createLabel("Parameters: "));
for( p in eventInfo.parameter)
panel.add(ui.createLabel(" - " + p + " = " + eventInfo.parameter[p]));
return panel;
}
The code uses a single "hidden" state (here visualized in a TextBox) and multiple SubmitButton's to allow the user to advance forward and backward through the form sequence, as well as to validate the contents of the form. The two extra SubmitButton's are "rewired" using ClientHandler's that simply modify the hidden state prior to form submission.
Notes
Note the use of the .setText(value) method in the client handler's. Using the Chrome browser I get weird runtime errors if I switch to either of the TextBox's .setValue(value) or .setValue(value, fireEvents) methods.
I tried (unsuccessfully) to implement this logic using a Script Property instead of the hidden TextBox. Instead of client handlers, this requires using server handlers. The behavior is erratic, suggesting to me that the asynchronous server-side events are occurring after the form submission event.
You could load different UI's on reading the parameters in your app.
The doGet(e) passes the parameters in the app's url. This way you could call your app with for example: ?myapp=1 (url parameter).
in your doGet you could read that parameter with: e.parameter.myapp
This way you could load different applications depending on the parameters that where passed.
You could just change your button with a link (to your own app, with different url parameters).
You could also do it with buttons and handlers but the above way has my preference.
If you want to use a button<>handler just change you main (first panel) and each time add a completely new panel to your app object. This way you would start from scratch (i.e. create a new application).
Currently there is no undo() function for Google Apps Script in the Spreadsheet/Sheet/Range classes. There were a few issues opened on the Issue Tracker, I can only find one now (I don't know what Triaged means): here.
There have been suggested workarounds using the DriveApp and revision history but I took a look around and didn't find anything (maybe it's buried?). In any case, an undo() function is incredibly necessary for so many different operations. I could only think of one kind of workaround, but I haven't been able to get it to work (the way the data is stored, I don't know if it's even possible). Here is some pseudo -
function onOpen () {
// Get all values in the sheet(s)
// Stringify this/each (matrix) using JSON.stringify
// Store this/each stringified value as a Script or User property (character limits, ignore for now)
}
function onEdit () {
// Get value of edited cell
// Compare to some value (restriction, desired value, etc.)
// If value is not what you want/expected, then:
// -----> get the stringified value and parse it back into an object (matrix)
// -----> get the old data of the current cell location (column, row)
// -----> replace current cell value with the old data
// -----> notifications, coloring cell, etc, whatever else you want
// If the value IS what you expected, then:
// -----> update the 'undoData' by getting all values and re-stringifying them
// and storing them as a new Script/User property
}
Basically, when the Spreadsheet is opened store all values as a Script/User property, and only reference them when certain cell criteria(on) are met. When you want to undo, get the old data that was stored at the current cell location, and replace the current cell's value with the old data. If the value doesn't need to be undone, then update the stored data to reflect changes made to the Spreadsheet.
So far my code has been a bust, and I think it's because the nested array structure is lost when the object is stringified and stored (e.g., it doesn't parse correctly). If anyone has written this kind of function, please share. Otherwise, suggestions for how to write this will be helpful.
Edit: These documents are incredibly static. The number of rows/columns will not change, nor will the location of the data. Implementing a get-all-data/store-all-data-type function for temporary revision history will actually suit my needs, if it is possible.
I had a similar problem when I needed to protect the sheet yet allow edits via a sidebar. My solution was to have two sheets (one hidden). If you edit the first sheet, this triggers the onEdit procedure and reloads the values from the second sheet. If you unhide and edit the second sheet, it reloads from the first. Works perfectly, and quite entertaining to delete data on mass and watch it self repair!
As long as you will not add or remove rows and columns, you can rely on the row and column numbers as indices for historic values that you store in ScriptDb.
function onEdit(e) {
// Exit if outside validation range
// Column 3 (C) for this example
var row = e.range.getRow();
var col = e.range.getColumn();
if (col !== 3) return;
if (row <= 1) return; // skip headers
var db = ScriptDb.getMyDb();
// Query database for history on this cell
var dbResult = db.query({type:"undoHistory",
row:row,
col:col});
if (dbResult.getSize() > 0) {
// Found historic value
var historicObject = dbResult.next();
}
else {
// First change for this cell; seed historic value
historicObject = db.save({type:"undoHistory",
row:row,
col:col,
value:''});
}
// Validate the change.
if (valueValid(e.value,row,col)) {
// update script db with this value
historicObject.value = e.value;
db.save(historicObject);
}
else {
// undo the change.
e.range.getSheet()
.getRange(row,col)
.setValue(historicObject.value);
}
}
You need to provide a function that validates your data values. Again, in this example we only care about data in one column, so the validation is very simple. If you needed to perform different types of validation different columns, for instance, then you could switch on the col parameter.
/**
* Test validity of edited value. Return true if it
* checks out, false if it doesn't.
*/
function valueValid( value, row, col ) {
var valid = false;
// Simple validation rule: must be a number between 1 and 5.
if (value >= 1 && value <= 5)
valid = true;
return valid;
}
Collaboration
This undo function will work for spreadsheets that are edited collaboratively, although there is a race condition around storing of historic values in the script database. If multiple users made a first edit to a cell at the same time, the database could end up with multiple objects representing that cell. On subsequent changes, the use of query() and the choice to pick only the first result ensures that only one of those multiples would be selected.
If this became a problem, it could be resolved by enclosing the function within a Lock.
Revised the answer from the group to allow for range when user selects multiple cells:
I have used what I would call "Dual Sheets".
One sheet acts as a backup / master and the other as the active sheet
/**
* 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
*/
function test_onEdit() {
onEdit({
user : Session.getActiveUser().getEmail(),
source : SpreadsheetApp.getActiveSpreadsheet(),
range : SpreadsheetApp.getActiveSpreadsheet().getActiveCell(),
value : SpreadsheetApp.getActiveSpreadsheet().getActiveCell().getValue(),
authMode : "LIMITED"
});
}
function onEdit() {
// This script prevents cells from being updated. When a user edits a cell on the master sheet,
// it is checked against the same cell on a helper sheet. If the value on the helper sheet is
// empty, the new value is stored on both sheets.
// If the value on the helper sheet is not empty, it is copied to the cell on the master sheet,
// effectively undoing the change.
// The exception is that the first few rows and the first few columns can be left free to edit by
// changing the firstDataRow and firstDataColumn variables below to greater than 1.
// To create the helper sheet, go to the master sheet and click the arrow in the sheet's tab at
// the tab bar at the bottom of the browser window and choose Duplicate, then rename the new sheet
// to Helper.
// To change a value that was entered previously, empty the corresponding cell on the helper sheet,
// then edit the cell on the master sheet.
// You can hide the helper sheet by clicking the arrow in the sheet's tab at the tab bar at the
// bottom of the browser window and choosing Hide Sheet from the pop-up menu, and when necessary,
// unhide it by choosing View > Hidden sheets > Helper.
// See https://productforums.google.com/d/topic/docs/gnrD6_XtZT0/discussion
// modify these variables per your requirements
var masterSheetName = "Master" // sheet where the cells are protected from updates
var helperSheetName = "Helper" // sheet where the values are copied for later checking
var ss = SpreadsheetApp.getActiveSpreadsheet();
var masterSheet = ss.getActiveSheet();
if (masterSheet.getName() != masterSheetName) return;
var masterRange = masterSheet.getActiveRange();
var helperSheet = ss.getSheetByName(helperSheetName);
var helperRange = helperSheet.getRange(masterRange.getA1Notation());
var newValue = masterRange.getValues();
var oldValue = helperRange.getValues();
Logger.log("newValue " + newValue);
Logger.log("oldValue " + oldValue);
Logger.log(typeof(oldValue));
if (oldValue == "" || isEmptyArrays(oldValue)) {
helperRange.setValues(newValue);
} else {
Logger.log(oldValue);
masterRange.setValues(oldValue);
}
}
// In case the user pasted multiple cells this will be checked
function isEmptyArrays(oldValues) {
if(oldValues.constructor === Array && oldValues.length > 0) {
for(var i=0;i<oldValues.length;i++) {
if(oldValues[i].length > 0 && (oldValues[i][0] != "")) {
return false;
}
}
}
return true;
}
I have a handler function that is updating a quite large UI with values from a spreadsheet. The rowindex of the sheet comes from a callbackElement as e.parameter.hidden (hidden is the widget that holds the value) and that I increment/decrement in the function.
I have another handler function that searches the spreadsheet for a string value and returns a rowindex that I assign to this same hidden widget.
If I trigger the 'search' handler first and then the 'display' handler, I have my UI updated with the 'found' data, which is nice but needs two separate clicks on two distinct buttons.
Now my question : how could I include in the 'search handler function' a call to the 'display handler' and pass all e.parameters BUT modify the value of the e.parameter.hidden only ?
I know e is an object with a lot of keys and values but I don't know how to manipulate just one value in there...
The code of the search handler is very short and goes like this :
function findname(e){ // this function is called by a handler triggered by a keypress on a textBox
var app = UiApp.getActiveApplication();
var hiddenD = app.getElementById('hiddenD');
var str = e.parameter.find ; // find is the textBox's name
var found = point(str);// point() is a function that returns the rowindex of the found value
hiddenD.setValue(found);// hiddenD is the hidden widget that I want to 'manipulate'
nextitem(e);// nextitem is the function that updates the UI and that is normally called by a handler on another button
return app
}
Well I hope this question is clear enough (this wasn't easy to explain), if not please ask ;-)
e is just a json data which you can manipulate by its key.
Code skeleton is like
searchHandlerFunction(e){
//Your all other sttements
//Assign the new value to hidden parameter
e.parameter.hidden = <your new value>;
DisplayFunction(e);
}
Why don't you simply store the rowIndex as a CacheService property ? I'm using this with a lot of success in lieu of hidden fields and hidden text boxes.
You can call them from anywhere and modify them anywhere.