Run replace script in Google App Maker - google-apps-script

I would like to add a server script that replaces special characters when a textbox is filled. The info for the "onValueEdit" routine states:
This script will run on the client whenever the value of this widget is edited by the user. The widget can be referenced using parameter widget and the new value of the widget is stored in newValue. Unlike onValueChange(), this runs only when a user changes the value of the widget; it won't run in response to bindings or when the value is set programmatically.
Therefore I have built the following server script that should take the text from the textbox, overwrite the special characters and replace the text in the textbox. But when I add the script to "onValueEdit" event, Google App Maker returns "function is undefined".
function cleanup(input, output) {
if (input !== null) {
output = input.trim();
output = output.replace('ß','ss');
output = output.replace('ä','ae');
output = output.replace('ö','oe');
output = output.replace('ü','ue');
return output;
}
}

In case you want to make this changes only on client side the ritgh way to do it will be adding this code to the onValueEdit event handler:
// onValueEdit input's event handler
if (newValue !== null) {
output = newValue.trim();
output = output.replace('ß','ss');
...
widget.value = output;
}
If you need to securely enforce this override prior to persisting to database, then you need to go with Model Events:
// onBeforeCreate and onBeforeSave events
if (record.FieldToChange !== null) {
record.FieldToChange = record.FieldToChange.trim();
record.FieldToChange = record.FieldToChange.replace('ß','ss');
...
}
With this approach you don't need any client code since all changes made on server should automatically sync back to client.

Related

Slate Foundry : Modify parameter of a Dropdown programmatically

In Slate foundry, I would modify parameter of a Dropdown programmatically.
Change the display value or disable value. But it doesn't work.
My code
const select ={{w_selectEndMonthYear}}
select.selectedValue = "202201"
select.selectedDisplayValue = "January 2022"
select.disabled = true
console.log({{w_selectEndMonthYear}})
return {{w_selectEndMonthYear.selectedDisplayValue}}
in my console, I have
{selectedDisplayValue: 'January 2022', selectedValue: '202201', disabled: 'true'}
It seems good, but neither the return nor the display of the widget changed
Thanks for your help
You're on the right track - to template the selected value of a dropdown list programmatically you need to change to the </> tab of the dropdown widget configuration and use Handlebars to provide a valid default selection for both the the selectedValue and selectedDisplayValue parameter.
Note that you cannot set widget properties from inside a Slate Function. You need to return the value (or a json object with multiple values to reference) and then use a Handlebar statement in the widget configuration to template the appropriate values into the widget configuration.
You can read a bit about this in the context of resetting widget selection state to default values in this section of the documentation.
You can also search your Foundry instance for some helpful Slate tutorials - find the Foundry Training and Resources project and navigate to the Reference Examples/App Building In Slate/1. Tutorials folder to find a series of interactive Slate apps demonstrating the many patterns for using Handlebars, Functions, and other Slate components.
The easiest way i via Functions, also to deliver the the content of dropdown.
Just a hint, to sort the content you need 'transformColumnSchemaToRowSchema' and 'transformRowSchemaToColumnSchema'
Here is a Slate JavaScript snippet for enabling/disabling a button:
Similar you could do this for drop-downs i think.
// Collect Inputs for Validation
// -----------------------------
var inputs = {
UserID: _.trim(_.toUpper({{w_pD_AdmUserAdd_UserID.text}})),
Foundry: _.trim({{w_pD_AdmUserAdd_Foundry.placeholder}}),
NameFirst: _.trim(_.toUpper({{w_pD_AdmUserAdd_NameFirst.text}})),
NameLast: _.trim(_.toUpper({{w_pD_AdmUserAdd_NameLast.text}})),
Email: _.trim(_.toUpper({{w_pD_AdmUserAdd_Email.text}})),
Team: {{w_pD_AdmUserAdd_Team.selectedValue}},
Company: {{w_pD_AdmUserAdd_Company.selectedValue}}
}
// Initialize Variables
// --------------------
var disable = false;
var messages = [];
// Implement Form Validation Checks
// --------------------------------
// Check if all the required fields have a value
if (!(inputs.UserID &&
inputs.Foundry &&
inputs.NameFirst &&
inputs.NameLast &&
inputs.Email &&
inputs.Team &&
inputs.Company
)){
disable = true;
messages.push("Please complete all required fields.");
}
var email_regex = /^(([^<>()[\]\\.,;:\s#\"]+(\.[^<>()[\]\\.,;:\s#\"]+)*)|(\".+\"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (inputs.Email && !email_regex.test(inputs.Email)){
messages.push('Please enter a valid email for "Email"')
disable = true;
}
var data_users = {{s_buyers.data}}
var index_email = data_users.str_email.indexOf(inputs.Email)
if (inputs.Email && index_email !== -1){
disable = true;
messages.push('This Email already exist')
}
var index_userid = data_users.str_buyer_id.indexOf(inputs.UserID)
if (inputs.UserID && index_userid !== -1){
disable = true;
messages.push('This User ID already exist')
}
return {
inputs,
disable,
messages
}

Passing additional parameters to an Apps Script event object

I'm having a lot of headaches with callbacks and scopes in Apps Script. I have the following function, that creates a Gmail Add-On section with a checkbox for each attached file on the currently open email:
function createAttachmentsSection( attachments ){
var widget = CardService.newSelectionInput()
.setFieldName( 'selectedFiles' )
.setType( CardService.SelectionInputType.CHECK_BOX )
.setOnChangeAction(
CardService.newAction()
.setFunctionName( 'checkboxAltered' )
);
for ( var i = 0 ; i < attachments.length; i++ )
widget.addItem( attachments[i].getName(), '' + i , true )
return CardService.newCardSection().addWidget( widget );
}
Everything is displayed as expected, but I want the user to be also able to select which files he wants to uncheck for later processing.
In my checkboxAltered function I want to handle user selection input actions, which is the only way to go (as far as I've learned from the documentation. Please tell me if I'm wrong!). It's currently an empty function, only used to check how little I understand:
function checkboxAltered( e ){
Logger.log( e );
Logger.log( selections.length );
}
e, the event object, is always undefined here. And selections, a (previously initialized and checked) global bool array with an item for each attachment, is always simply [] inside checkboxAltered.
What am I doing wrong?
How can I use setParameters( Object ) to tell inside the callback function which checkbox was clicked by the user?
Are global variables not accessible from callback functions?
Looking at the CardService documentation the Action class has a setParameters() method. If you want to pass custom parameters to the callback you'll need to use that method.

Only one type of action can execute?

I going to show my problem with an example:
I have a button. When clicked, it creates a mail draft based on the TextInputFields in the add-on.
I have a validate function, which can say if the fields filled right or not.
If I want to notify the user somehow about the wrong fields, I have to create a notify or rebuild the card with error information. These actions can be returned in a normal Action, but not with a composeAction (because composeAction has to return with builded draft), so I have to register a composeAction and a simple action to the button.
When I clicked this kind of button, only one of the action execute and the other do nothing.
Some code about how I tried to implement:
section.addWidget(CardService.newTextButton()
.setText('Validate and Create')
.setComposeAction(CardService.newAction().setFunction('doIt'), CardService.ComposedEmailType.STANDALONE_DRAFT)
.setOnClickAction(CardService.newAction().setFunction('notify')));
ActionFunctions:
function doIt(event){
validate the event['formInput'] object;
if(valid the formInput)
create andr return the draft;
else
return null;
}
function notify(event){
validate the event['formInput'] object;
if(valid the formInput)
return null;
else
return notify or rebuilded card with error info;
}
Mostly the simple action run, and the compose do nothing. If I place Logger.log() functions in the callback function, only one appears on api log.
Anyone have tried before validate and create draft at the same click?
How about this:
var action=CardService.newAction().setFunctionName('myFunction');
var validateCreateButton=CardService.newTextButton()
.setText('Validate & Create')
.setOnClickAction(action);
section.addWidget(validateCreateButton);
function myFunction(e) {
doit(e);
notify(e);
}

WinJS variable/object scope, settings, and events?

I am not sure what the proper heading / title for this question should be. I am new to WinJS and am coming from a .NET webform and winclient background.
Here is my scenario. I have a navigation WinJS application. My structure is:
default.html
(navigation controller)
(settings flyout)
pages/Home.html
pages/Page2.html
So at the top of the default.js file, it sets the following variables:
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
var nav = WinJS.Navigation;
It seems like I cannot use these variables anywhere inside my settings flyout or any of my pages:ready functions. They are only scoped to the default.js?
In the same regard, are there resources on the interwebs (links) that show how to properly share variables, events, and data between each of my "pages"?
The scenario that I immediately need to overcome is settings. In my settings flyout, I read and allow the user to optionally set the following application setting:
var applicationData = Windows.Storage.ApplicationData.current;
var localSettings = applicationData.localSettings;
localSettings.values["appLocation"] = {string set by the user};
I want to respond to that event in either my default.js file or even one of my navigation pages but I don't know where to "listen". My gut is to listen for the afterhide event but how do I scope that back to the page where I want to listen from?
Bryan. codefoster here. If you move the lines you mentioned...
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
var nav = WinJS.Navigation;
...up and out of the immediate function, they'll be in global scope and you'll have access to them everywhere. That's one of the first things I do in my apps. You'll hear warnings about using global scope, but what people are trying to avoid is the pattern of dropping everything in global scope. As long as you control what you put in there, you're fine.
So put them before the beginning of the immediate function on default.js...
//stuff here is scoped globally
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
var nav = WinJS.Navigation;
(function () {
//stuff here is scoped to this file only
})();
If you are saving some data and only need it in memory, you can just hang it off the app variable instead of saving it into local storage. That will make it available to the whole app.
//on Page2.js
app.myCustomVariable = "some value";
//on Page3.js
if(app.myCustomVariable == "some value") ...
Regarding your immediate need:
like mentioned in the other answer, you can use datachanged event.
Regards sharing variables:
If there are variables that you would like to keep global to the application, they can be placed outside the anonymous function like mentioned in the Jeremy answer. Typically, that is done in default.js. Need to ensure that scripts using the global variables are placed after the script defining the global variable - in default.html. Typically - such variable will point to singleton class. For example: I use it in one of my apps to store authclient/serviceclient for the backend service for the app. That way - the view models of the multiple pages need not create instance of the object or reference it under WinJS namespace.
WinJS has also concept of Namespace which lets you organize your functions and classes. Example:
WinJS.Namespace.define('Utils.Http',
{
stringifyParameters: function stringifyParameters(parameters)
{
var result = '';
for (var parameterName in parameters)
{
result += encodeURIComponent(parameterName) + '=' + encodeURIComponent(parameters[parameterName]) + '&';
}
if (result.length > 0)
{
result = result.substr(0, result.length - 1);
}
return result;
},
}
When navigating to a page using WinJS.Navigation.navigate, second argument initialState is available as options parameter to the ready event handler for the page. This would be recommended way to pass arguments to the page unless this it is application data or session state. Application data/session state needs to be handled separately and needs a separate discussion on its own. Application navigation history is persisted by the winjs library; it ensures that if the app is launched again after suspension - options will be passed again to the page when navigated. It is good to keep the properties in options object as simple primitive types.
Regards events:
Typically, apps consume events from winjs library. That can be done by registering the event handler using addEventListener or setting event properties like onclick etc. on the element. Event handlers are typically registered in the ready event handler for the page.
If you are writing your own custom control or sometimes in your view model, you may have to expose custom events. Winjs.UI.DOMEventMixin, WinJS.Utilities.createEventProperties can be mixed with your class using WinJS.Class.mix. Example:
WinJS.Class.mix(MyViewModel,
WinJS.Utilities.createEventProperties('customEvent'),
WinJS.UI.DOMEventMixin);
Most often used is binding to make your view model - observable. Refer the respective samples and api documentation for details. Example:
WinJS.Class.mix(MyViewModel,
WinJS.Binding.mixin,
WinJS.Binding.expandProperties({ items: '' }));
Here is what I ended up doing which is kinda of a combination of all the answers given:
Created a ViewModel.Settings.js file:
(function () {
"use strict";
WinJS.Namespace.define("ViewModel", {
Setting: WinJS.Binding.as({
Name: '',
Value: ''
}),
SettingsList: new WinJS.Binding.List(),
});
})();
Added that file to my default.html (navigation container page)
<script src="/js/VMs/ViewModel.Settings.js"></script>
Add the following to set the defaults and start 'listening' for changes
//add some fake settings (defaults on app load)
ViewModel.SettingsList.push({
Name: "favorite-color",
Value: "red"
});
// listen for events
var vm = ViewModel.SettingsList;
vm.oniteminserted = function (e) {
console.log("item added");
}
vm.onitemmutated = function (e) {
console.log("item mutated");
}
vm.onitemchanged = function (e) {
console.log("item changed");
}
vm.onitemremoved = function (e) {
console.log("item removed");
}
Then, within my application (pages) or my settings page, I can cause the settings events to be fired:
// thie fires the oniteminserted
ViewModel.SettingsList.push({
Name: "favorite-sport",
Value: "Baseball"
});
// this fires the itemmutated event
ViewModel.SettingsList.getAt(0).Value = "yellow";
ViewModel.SettingsList.notifyMutated(0);
// this fires the itemchanged event
ViewModel.SettingsList.setAt(0, {
Name: "favorite-color",
Value: "blue"
});
// this fires the itemremoved event
ViewModel.SettingsList.pop(); // removes the last item
When you change data that needs to be updated in real time, call applicationData.signalDataChanged(). Then in the places that care about getting change notifications, listen to the datachanged on the applicationData object. This is also the event that is raised when roaming settings are synchronized between computers.
I've found that many times, an instant notification (raised event) is unnecessary, though. I just query the setting again when the value is needed (in ready for example).

a clear example of how to use Google UI Builder and Apps script

I have searched everywhere and cannot find a clear example of how to use Googles UI Builder and apps script. I have no clue what I'm missing. I think this should be simple :v/ YES, I've read all Googles docs, watched vids etc - several times - there is no combination of GUIB (Google's UI Builder) and a callback handler function, that I can find.
EDIT: there are examples for SpreadSheets - not GSites
What I need to do:
I would like to embed a textbox and button to collect a search phrase from a user, on a Google site page. I have built the very simple UI with a single flowpanel, textbox and button, but can only ever get "Undefined" returned from Logger.log() no matter what I do (see code below).
A bit of a rant:
I have been very careful to name, and call by the right names. I've tried using a formpanel BUT in GUIB, you can only put ONE widget in it?! ...AND a submit button will only go into a formpanel - huh - I can't put my text box in as well!? (Why bother with the formpanel then - I don't get that! ...yeah I know about doPost() automatically being called on submit). I want the widgets to remain active and not disappear after one use, so maybe formpanel/submitbutton won't work anyway - or isn't the right way to do it?
Down to business:
At any rate, what I've tried is to put the regular button and text box in a flowpanel with the following code...
EDIT: I deleted my original content here and reposted this section...
// Google Sites and UIBuilder (GUIB) - kgingeri (Karl)
// - this script is embedded in a GSite page via: Insert -> Apps Script Gadget.
//
// Withing GUIB I have defined:
// - a FlowPanel named 'pnlMain'
// - inside that a textBox named 'tbxQuery' and a button called 'btnSearch'
// - for btnSearch, I have defined (in the Events subsection) a callback function
// btnSearchHandler (see it below doGet() here. I expanded the [+] beside that
// and entered 'tbxQuery'
//
// the GUIB compnent tree looks like this...
//
// [-] testGui
// [-] pnlMain
// btnSearch
// tbxQuery
//
// btnSearch Event section looks something like this...
//
// Events
// On Mouse Clicks
// [X][btnSearchHandler][-]
// [tbxQuery ]<--'
// [Add Server]
// ...
//
// So...
// 1) when the page is opened, the doGet() function is called, showing the defined UI
// 2) when text is entered into the textBox and the button is clicked
// 3) the data from tbxQuery is **SUPPOSED TO BE** returned as e.parameter.tbxQuery
// in the function 'btnSearchHandler(e)' **BUT IS NOT** :v(
//
// (this functionality appears to work in a spreadsheet?! - weird?!)
//
// [ predefined function --- Google calls on page open ]
//
// ...this works 'as advertised' ;v)
//
function doGet(e) {
var app = UiApp.createApplication();
app.add(app.loadComponent("testGui")); // ...the title that shows in G/UIBuilder
return app;
}
//
// [ callBack for when a button is clicked ]
//
// ...I always get 'Resp: [Undefined]' returned in the View -> Logs menu?!
// ...I also tried to put 'pnlMain' in the Event [+] param, no go :v(
//
function btnSearchHandler(e) {
var resp = e.parameter.tbxQuery // ...the data I want in the textBox widget
Logger.log('Resp: [' + e.parameter.tbxQuery + ']');
// ...more code to come, once this works!
}
I've also tried adding code to manually set handlers etc in doGet(), and not use GUIB Event settings, but to no avail either.
Conclusion?
What Gives? Do I have to hand-code the GUIs and not use GUIB? I know it's a simple one this time, but if I can get this working I can sure see being much nicer to build other apps with GUIB! Can anyone give me or point me to a clear example?!
Thanks for reading!
here is a shared spreadsheet with an example of GUI builder
when you're in the GUI builder look at the properties of the element you want to trigger a function, at the end of the parameter list there is an 'EVENT' properties where you can add the function name and the callbackElements as well. !
Hoping it's clear enough,
cheers,
Serge
EDIT : if you want to have a look at a more complex example please open this one (create a copy of it to make it editable) or see it working here, I think you might be convinced that the GUI builder is a really powerfull tool .
Many thanks to Serge Insas!!
The answer is as shown below - I had missed two things:
the small [+] beside the On Mouse Click server handler - to add
a parameter to return
the Name is what is used NOT ID - set in
Input Fields section of tbxQuery
(NOTE: non-data elements don't have names - so fplMain has only an ID, but still works)
So, here is the resulting code, and comments describing GUIB settings:
// Google Sites and UIBuilder (GUIB) - kgingeri (Karl)
// - this script is embedded in a GSite page via: Insert -> Apps Script Gadget.
//
// Withing GUIB I have defined:
// - a FlowPanel named 'fplMain'
// - inside that, a textBox named 'tbxQuery' (see Input Fields section - this in NOT ID)
// and a button called 'btnSearch'
// - for btnSearch, I have defined (in the Events subsection) a callback function
// btnSearchHandler (see it below doGet() here). I expanded the [+] beside that,
// and entered "fplMain" as the return param (it will return all data elements)
//
// the GUIB compnent tree looks like this...
//
// [-] SearchGui
// [-] fplMain
// btnSearch
// tbxQuery
//
// "tbxQuery" Input Fields param, "Name"... **THIS MUST BE SET!
//
// Input Fields
// ...
// Name
// [tbxQuery ]
//
// "btnSearch" Event section looks like this...
//
// Events
// On Mouse Clicks
// [X][btnSearchHandler][-]
// [fplMain ]<--'
// [Add Server]
// ...
//
// So...
// 1) when the page is opened, the doGet() function is called, showing the defined UI
// 2) when text is entered into the textBox and the button is clicked
// 3) the data from tbxQuery is returned as e.parameter.tbxQuery (as would be any other
// params under the flow panel "fplMain") in the function 'btnSearchHandler(e)'
//
// [ predefined function --- Google calls on page open ]
//
function doGet(e) {
var app = UiApp.createApplication();
app.add(app.loadComponent("SearchGUI")); // ...the title you choose in G/UIBuilder
return app;
}
//
// [ callBack for when a button is clicked ]
//
function btnSearchHandler(e) {
var resp = e.parameter.tbxQuery // ...the data in the textBox widget
Logger.log('Resp: [' + e.parameter.tbxQuery + ']');
//
// ...more code goes here, to do something with the returned data
//
}