I created an array in a separate GS file using the code provided below. I tried calling it in my HTML file. My goal is to compare the contents the array to the parameter email. However, the value returned by google.script.run.withSuccessHandler() is undefined
//in GS
function mailGetter()
{
//open sheet
var sheet = SpreadsheetApp.openByUrl("https://sheet.url").getSheetByName("Email Sheet").activate();
//get size of given row range
var row_data_email = sheet.getRange("C2:C").getValues();
var emailArray = row_data_email.join().split(',').filter(Boolean);
Logger.log(emailArray);
return emailArray;
}
//in HTML
function checkEmail(email)
{
var reg1 = /^[a-z0-9._%+-]+#[a-z0-9.-]+\.[a-z]{2,4}$/;
var arraySize = google.script.run.withSuccessHandler(misc).sizeGetter();
console.log(arraySize);
var emailArray = new Array(arraySize);
emailArray = google.script.run.withSuccessHandler(misc).mailGetter();
console.log(emailArray);
if (reg1.test(email) == false)
{
emails.style.border = "1px solid red";
document.getElementById('submitBtn').disabled = true;
}
else if (reg1.test(email) == true)
{
emails.style.border = "1px solid green";
document.getElementById('submitBtn').disabled = false;
}
for (var row = 0; row < arraySize; row++)
{
if (emailArray[row][0] == email)
{
emails.style.border = "1px solid green";
document.getElementById('submitBtn').disabled = false;
break;
}
else if (emailArray[row][0] != email)
{
emails.style.border = "1px solid red";
document.getElementById('submitBtn').disabled = true;
}
}
}
function misc()
{
console.log("Pass");
}
Issue:
Using a asynchronous function's(google.script.run) return value, which will always be undefined.
Solution:
Use successHandler as mentioned in another answer or We can use promises with async/await.
Snippet:
/*Create a promise around old callback api*/
const p = func =>
new Promise(resolve=>
google.script.run.withSuccessHandler(resolve)[func]()
);
async function checkEmail(email) //modified
{
var arraySize = await p('sizeGetter');//Wait to resolve
console.log(arraySize);
//var emailArray = new Array(arraySize);
var emailArray = await p('mailGetter');//Wait to resolve
console.log(emailArray);
//....
}
Note:
It's better to reduce the number of calls to the server. If you can combine both Getters to a single server function, it'll be better.
The above is a snippet showing how to use async/await. But if you wait for each response from the server as shown above, your front end/UI will be slow. Wait only if absolutely necessary. Calls to server should be non-blocking/asynchronous.
References:
Promises
async
await
Issue is in these lines:
emailArray = google.script.run.withSuccessHandler(misc).mailGetter();
console.log(emailArray);
You're trying to execute mailGetter() and expecting it to return value which you're storing in emailArray but this method is asynchronous and does not return directly
Rather you'll get the value in callback which you have defined as SuccessHandler
Suggested solutions :
Calling Apps Script functions from a template : https://developers.google.com/apps-script/guides/html/templates#apps_script_code_in_scriptlets
Calling Apps Script APIs directly : https://developers.google.com/apps-script/guides/html/templates#calling_apps_script_apis_directly
Pushing variables to templates : https://developers.google.com/apps-script/guides/html/templates#pushing_variables_to_templates
Reference : https://developers.google.com/apps-script/guides/html/reference/run#myFunction(...)
Related
I'm new to Google Data Studio and looking into building a community connector for our Saas service.
For the configuration section, I need to use the Stepped Configuration process. Basically, I nested set of drop-down lists.
However, I need the data to populate those lists to come from my API. I have the REST service endpoints defined, but I cannot find any documenation/examples of how I'd configure this in the getConfig section of the community connector.
Does anyone have a working example I could use as reference?
In reviewing the documentation, there is a section on stepped configurations, which is what I am looking for. You can find that example here: https://developers.google.com/datastudio/connector/stepped-configuration#dynamic_dropdowns
In this example, they show the following for defining the dropdown values.
Notice for the states, they have hard-coded the values for "Illinois" and "California".
My question is, how can I dynamically call API to retrieve values to populate this list? I have 3 nested dropdowns, each with a separate API call, using the answer from previous dropdown to drive the next.
For example first API might be http://myapi.com/countries which returns list of countries.
When they select country, next API call might be http://myapi.com/states?country=US
etc.
config.newSelectSingle()
.setId("state")
.setName("State")
// Set isDynamic to true so any changes to State will clear the city
// selections.
.setIsDynamic(true)
.addOption(config.newOptionBuilder().setLabel("Illinois").setValue("IL"))
.addOption(config.newOptionBuilder().setLabel("California").setValue("CA"));
if (!isFirstRequest) {
var city = config.newSelectSingle()
.setId("city")
.setName("City");
var cityOptions = optionsForState(configParams.state);
cityOptions.forEach(function(labelAndValue) {
var cityLabel = labelAndValue[0];
var cityValue = labelAndValue[1];
city.addOption(config.newOptionBuilder().setLabel(cityLabel).setValue(cityValue));
});
}
return config.build();
}
Worked through the issues I was having. For others who might have hit similiar issues, here's my working getConfig() method.
function getConfig(request) {
var config = cc.getConfig();
var configParams = request.configParams;
var isFirstRequest = configParams === undefined;
if (configParams ===undefined || configParams.tab ===undefined) {
config.setIsSteppedConfig(true);
}
var url ='https://<yourAPIURL>';
var userProperties = PropertiesService.getUserProperties();
var key = userProperties.getProperty('dscc.key');
var mykey ="Bearer " + key
var options = {
"method" : "GET",
"headers" : {
"AUTHORIZATION" : mykey,
"cache-control": "no-cache"
}
};
var response = UrlFetchApp.fetch(url,options);
var parsedResponse = JSON.parse(response);
var zoneControl = config.newSelectSingle()
.setId("zone")
.setName("Zone")
.setIsDynamic(true);
parsedResponse.map(function(itm) {
zoneControl.addOption(config.newOptionBuilder().setLabel(itm.name).setValue(itm.id))
});
if(configParams !==undefined && configParams.zone !==undefined){
var blockurl ='https://<yourAPIURL>?zoneid='+ configParams.zone;
var blockResponse = UrlFetchApp.fetch(blockurl,options);
var parsedBlockResponse = JSON.parse(blockResponse);
var blockControl = config.newSelectSingle()
.setId("block")
.setName("Block")
.setIsDynamic(true);
parsedBlockResponse.map(function(itm) {
blockControl.addOption(config.newOptionBuilder().setLabel(itm.name).setValue(itm.blockKey))
});
}
if(configParams !==undefined && configParams.block !==undefined){
var taburl =''https://<yourAPIURL>?blockKey='+ configParams.block;
var tabResponse = UrlFetchApp.fetch(taburl,options);
var parsedTabResponse = JSON.parse(tabResponse);
var tabControl = config.newSelectSingle()
.setId("tab")
.setName("Tab")
parsedTabResponse.map(function(itm) {
tabControl.addOption(config.newOptionBuilder().setLabel(itm.name).setValue(itm.internalname))
});
}
return config.build();
}
without testing the code:
function getConfig(request) {
var configParams = request.configParams;
var isFirstRequest = configParams === undefined;
var lst=["A","B","C"]; // your values obtained from REST
var tmp=config.newSelectSingle(); //add element to side
var element=tmp.setId("state").setName("State").setIsDynamic(true); // set name and id
for(var i in lst) // set all the values:
{
element = element.addOption(config.newOptionBuilder().setLabel(lst[i]).setValue(lst[i]))
}
if(isFirstRequest || configParams.state==undefined) // no state selected yet
{
config.setIsSteppedConfig(true); // stop here
}
else
{
// next dropdown element,
// Rest API with element set to: configParams.state
var lst2= ["x","y","z"]
var tmp2=config.newSelectSingle(); //add element to side
var element2=tmp2.setId("element2").setName("Element 2 depends on "+configParams.state).setIsDynamic(true); // set name and id
for(var i in lst2) // set all the values:
{
element2 = element2.addOption(config.newOptionBuilder().setLabel(lst2[i]).setValue(lst2[i]))
}
// code for 3rd
}
}
If the user changes the first dropdown value alle other drop downs have to be reset. This may be a bit tricky.
I am new to react-native and calling a function inside a fucntion.
I have done as below so far :
Step 1 : Created a function _snapshotToArray to convert the firebase snapshot to Arrray.
_snapshotToArray(snapshot) {
var returnArr = [];
snapshot.forEach(function(childSnapshot) {
var item = childSnapshot.val();
item.key = childSnapshot.key;
returnArr.push(item);
});
return returnArr;
}
Step 2 : Created another function as below and calling _snapshotToArray inside it.
_readUserDataFromFirebaseConsole() {//once and on
firebase.database().ref('Users/').on('value', function (snapshot) {
console.log(this._snapshotToArray(snapshot));
Toast.show(this._snapshotToArray(snapshot),Toast.LONG);
});
}
Talking about this call :
console.log(this._snapshotToArray(snapshot));
When I press CTRL+CLick, it not letting me to navigate to body of the fuction _snapshotToArray.
In Device am getting below error :
_snapshotToArray is not defined
What might be the issue ?
I'm not at my PC right now, so I cannot test it, but from looking at your code, you need to use a different function notation to allow the varibale access of/from parent methods and parent class.
_snapshotToArray = snapshot => {
var returnArr = [];
snapshot.forEach(function(childSnapshot) {
var item = childSnapshot.val();
item.key = childSnapshot.key;
returnArr.push(item);
});
return returnArr;
}
and
_readUserDataFromFirebaseConsole = () => {
firebase.database().ref('Users/').on('value', snapshot => {
console.log(this._snapshotToArray(snapshot));
Toast.show(this._snapshotToArray(snapshot),Toast.LONG);
});
}
I have a factory in my AngularJS single page application that parses a given date against a JSON file to return season and week-number in season. I am currently calling the JSON file twice in each method $http.get('content/calendar.json').success(function(data) {....
How can i factor out the call to do it once regardless of how many methods?
emmanuel.factory('DayService', function($http, $q){
var obj = {};
obj.season = function(d){
// receives a mm/dd/yyyy string parses against Calendar service for liturgical season
d = new Date(d);
var day = d.getTime();
var promise = $q.defer();
var temp;
$http.get('content/calendar.json').success(function(data) {
for (var i=0; i<data.calendar.seasons.season.length; i++){
var start = new Date(data.calendar.seasons.season[i].start);
var end = new Date(data.calendar.seasons.season[i].end);
end.setHours(23,59);
//sets the time to be the last minute of the season
if (day >= start && day <= end){
//if given time fits within start and end dates in calendar then return season
temp = data.calendar.seasons.season[i].name;
//console.log(temp);
promise.resolve(temp);
break;
}
}
});
return promise.promise;
}
obj.weekInSeason = function(d){
//receives a date format mm/dd/yyyy
var promise = $q.defer();
$http.get('content/calendar.json').success(function(data) {
for (var i=0; i<data.calendar.seasons.season.length; i++){
d = new Date(d);
var day = d.getTime();
var end = new Date(data.calendar.seasons.season[i].end);
end.setHours(23,59);
end = end.getTime();
var diff = end - day;
if (parseFloat(diff) > 0){
var start = new Date(data.calendar.seasons.season[i].start);
start = start.getTime();
var startDiff = day - start;
var week = parseInt(startDiff /(1000*60*60*24*7))+1;
promise.resolve(week);
break;
}
}
});
return promise.promise;
}
obj.getData = function (d) {
console.log('DayService.getData')
console.log(today)
var data = $q.all([
this.season(d),
this.weekInSeason(d)
]);
return data;
};
return obj;
});
This solution assumes that content/calendar.json never changes.
I have answered a question which can help you in this problem one way or another. Basically you must fetch all necessary configurations/settings before the application bootstraps. Manually bootstrap the application, this means that you must remove the ng-app directive in your html.
Steps:
[1] Create bootstrapper.js as instructed in the answered question I have mentioned above. Basically, it should look like this(Note: You can add more configuration urls in urlMap, if you need to add more settings in your application before it bootstraps):
angular.injector(['ng']).invoke(function($http, $q) {
var urlMap = {
$calendar: 'content/calendar.json'
};
var settings = {};
var promises = [];
var appConfig = angular.module('app.settings', []);
angular.forEach(urlMap, function(url, key) {
promises.push($http.get(url).success(function(data) {
settings[key] = data;
}));
});
$q.all(promises).then(function() {
bootstrap(settings);
}).catch(function() {
bootstrap();
});
function bootstrap(settings) {
appConfig.value('Settings', settings);
angular.element(document).ready(function() {
angular.bootstrap(document, ['app', 'app.settings']);
});
}
});
[2] Assuming that the name of your main module is app within app.js:
angular.module('app', [])
.factory('DayService', function(Settings){
var calendar = Settings.$calendar,
season = calendar.seasons.season,
obj = {};
obj.season = function(d){
var day = new Date(d).getTime(),
start, end, value;
for (var i = 0; i < season.length; i++){
start = new Date(season[i].start);
end = new Date(season[i].end);
end.setHours(23,59);
if (day >= start && day <= end){
value = season[i].name;
break;
}
}
return value;
};
obj.weekInSeason = function(d){
var day = new Date(d).getTime(),
end, diff, start, startDiff, week;
for (var i = 0; i < season.length; i++){
end = new Date(season[i].end);
end.setHours(23,59);
end = end.getTime();
diff = end - day;
if (parseFloat(diff) > 0){
start = new Date(season[i].start);
start = start.getTime();
startDiff = day - start;
week = parseInt(startDiff /(1000*60*60*24*7))+1;
break;
}
}
return week;
};
return obj;
});
[3] Controller Usage(Example):
angular.module('app')
.controller('SampleController', function(DayService) {
console.log(DayService.season(3));
console.log(DayService.weekInSeason(3));
});
Another Note: Use .run() to check if Settings === null - if this is true, you can direct to an error page or any page that displays the problem(This means that the application bootstrapped but one of the requested configuration failed).
UPDATE:
I checked the link you have provided, and it seems that the version you are using is AngularJS v1.0.8, which does not have a .catch() method in their $q promise implementation.
You have the following options to consider in solving this problem:
-1 Change the AngularJS version you are using to the latest stable version 1.2.23.
Note that this option may break some of your code that is highly reliant on the version that you are using.
-2 Change this block:
$q.all(promises).then(function() {
bootstrap(settings);
}).catch(function() {
bootstrap();
});
to:
$q.all(promises).then(function() {
bootstrap(settings);
}, function() {
bootstrap();
});
This option is safer if you already have existing code that relies on the current AngularJS version you are using But I would suggest you change to the current stable version as it has more facilities and fixes than the one you are currently using.
Use your scope and closure of the factory to store the value of the response to the http call. I created an object called calData, which happens to already be a promise!
This gives you the ability to kick a few things off when the first call to the factory is made by running an IIFE (this is the function called initService), and everything chains together to resolve after the data is loaded.
.factory('dayService', function dayServiceFactory($http, $q){
var getCalData = $q.defer();
var calData = gettingData.promise; // null/undefined until _loadData is called and resolved
function _loadData(){
var deferred = $q.defer();
$http.get('content/calendar.json').success(function(data) {
calData.seasons = data.calendar.seasons; // your code seems to always use at least calendar.seasons, so easier to assign that
deferred.resolve(data);
});
return deferred.promise;
}
// this function will automatically run and load data the first time the factory is executed
(function initService(){
_loadData().then(){
// here is where you will build all your functions to assign properties to calData.seasons or any other child property of calData;
calData.getSeason = function(){
for (var i=0; i<data.calendar.seasons.season.length; i++){
// code here
}
}// function to get day using calData.seasons
calData.weekInSeason = function(){}
getCalData.resolve(); // this resolves the data in the outer scope
}
}());
return calData; // returns the promise, and will execute the first time called
});
To use this in a controller, make sure to either resolve the service before you instantiate the controller, or withing the controller, use your assignments of the data after it has resolved. (Bound values will auto-update when it's resolved)
dayService.then(function(){
// now you can use this:
var week = dayService.weekInSeason();
})
You can create separate method for getting calendar data and chain promises in getData method:
emmanuel.factory('DayService', ['$q', '$timeout', '$log',
function($q, $timeout, $log) {
return {
season: season,
weekInSeason: weekInSeason,
getData: getData
};
function season(d) {
$log.log('season called');
return getCalendar(d).then(function(calendar) {
return getSeason(d, calendar);
});
}
function weekInSeason(d) {
$log.log('weekInSeason called');
return getCalendar(d).then(function(calendar) {
return getWeekInSeason(d, calendar);
});
}
function getData(d) {
$log.log('getData called');
return getCalendar(d).then(
function(calendar) {
return $q.all({
season: getSeason(d, calendar),
weekInSeason: getWeekInSeason(d, calendar)
});
}
);
}
function getSeason(date, calendar) {
$log.log('getSeason called');
return {
date: date,
calendar: calendar,
method: 'getSeason'
};
}
function getWeekInSeason(date, calendar) {
$log.log('getWeekInSeason called');
return {
date: date,
calendar: calendar,
method: 'getWeekInSeason'
};
}
function getCalendar(d) {
$log.log('getCalendar called');
var deferred = $q.defer();
$timeout(function() {
deferred.resolve(12345);
}, 2000);
return deferred.promise;
}
}
]);
Also, if calendar.json doesn't changed during application lifetime, you can cache calendar.json ajax request result as suggested by #runTarm
Plunker
I'm using bluebird for the control flow in my application, I'm trying to implement promisify to extend my recursive function into a promise, but it seems like the "then" method never got executed
I'm doing a mapping from one JSON object to another, the find function looks recursively into the JSON properties and returns the property based on an specific condition.
var promise = require("bluebird");
var mapToModel = function(res){
// res is a json structure
var processes = res.definitions.process;
var diagrams = res.definitions.BPMNDiagram;
var documents = [];
for(var prop in processes) {
if(processes.hasOwnProperty(prop)){
var propertyNames = Object.getOwnPropertyNames(processes[prop]);
for(var property in processes[prop]){
var mapping ={};
if(property==="$"){
//do something with the process
}else{
//shapes
mapping.hash = hash.hashCode(new Date().toString());
mapping.type = property;
mapping.value = processes[prop][property];
var bpmnItem = findPromise(processes[prop][property], function(x) {return x.$.id;}).then(function(value){
//I'm not reaching this point
var bpmnId = value.$.id;
console.log(value);
if(bpmnId!=undefined){
console.log("return:"+ bpmnId);
}
});
documents.push(mapping);
}
}
}
return documents;
}
}
var findPromise = promise.promisify(find);
function find(items,f) {
for(var key in items) {
var elem = items[key];
if (f(elem)) { return elem;}
if(typeof elem === "object") {
find(elem,f); // call recursively
}
}
}
The Bluebird promisify method works on the accepted callback convention for NodeJS - nodebacks.
Nodebacks are in the specific format of someOp(function(err,result){ that is - the first argument is always an error.
In fact, your find method is not even asynchronous, so there is no reason to promisify it to begin with. You can simply call it as it is.
Generally, you should not promisify synchronous functions, you just call them normally. In fact, you don't seem to have any asynchronous operation in your code - so you should not be using promises at all in it.
You can simply do:
mapping.value = processes[prop][property];
var value = find(processes[prop][property], function(x) {return x.$.id;});
var bpmnId = value.$.id;
console.log(value);
if(bpmnId!=undefined){
console.log("return:"+ bpmnId);
}
Remember, Promises are an abstraction over an eventual result. You keep doing everything synchronous just like you did before.
I want to write a function in JS where I will loop through a tables in my indexed DB and get the maximum value of last modified of table and return that
function readData(){
var trans = '';
trans = idb.transaction(["tableName"],'readonly'); // Create the transaction
var request = trans.objectStore("tableName").openCursor();
request.onsuccess = function(e) {
var cursor = request.result || e.result;
if(cursor) {
// logic to and find maximum
} else {
return // max last modified
}
cursor.continue();
}
}
IMP--Since onsuccess method is asynchronous how can i make it synchronous? so that my method readData() will return only when max last modified record is found successfully. I can call this method(readData()) synchronously to get last modified record of 2-3 tables if I want.
The sync API is only available in a webworker. So this would be the first requirement. (As far as I know only IE10 supports this at the moment)
An other shot you can give is working with JS 1.7 and use the yield keyword. For more information about it look here
I would sugest to work with a callbakck method that you call when you reached the latest value.
function readData(callback){
var trans = '';
trans = idb.transaction(["tableName"],'readonly'); //Create the transaction
var request = trans.objectStore("tableName").openCursor();
var maxKey;
request.onsuccess = function(e) {
var cursor = request.result || e.result;
if(cursor.value){
//logic to and find maximum
maxKey = cursor.primaryKey
cursor.continue();
}
}
trans.oncomplete = function(e) {
callback(maxKey);
}
}
IndexedDB API in top frame is async. async cannot be synchronous. But you can read all tables in single transaction.