Excel JS API: write, calculate and load values from cells with custom functions - javascript

The intention is to write the formula (custom function) to the cell, calculate it, load values and retrieve them in a single function.
function myFunc() {
Excel.run(function (ctx) {
var fExcel = '=SUM(1,2)';
var fCustom = '=custFunc()';
var rng = ctx.workbook.worksheets.getActiveWorksheet().getRange('A1');
//rng.formulas = [[fExcel]]; // works OK
rng.formulas = [[fCustom]]; // values are #GETTING_DATA
// try different calc calls
rng.load("values");
return ctx.sync().then(function () {
console.log(rng.values);
});
});
}
For built-in Excel functions, everything works as expected and console logs a value 3 after ctx.sync(). With custom functions (that send a request to the external server to compute the result) the values are '#GETTING_DATA'. I've tried all the following things before rng.load("values"); to trigger the calculation, but nothing have worked so far:
rng.calculate();
var s = ctx.workbook.worksheets.getActiveWorksheet();
s.calculate(true);
ctx.workbook.application.calculate('Full');
Is there a way to trigger the calculation of custom functions and make sure that the values are available after the ctx.sync()?

Interesting scenario!
Today, this may be feasible leveraging the onCalculate event but the caveat is you it will fire 2x when you're custom function is calculating.
This is because your custom function first will show a #GETTING_DATA, while it calculates in the background.
This gives the user back control while your functions are still evaluating, allowing the application to be more responsive. This behavior differs from VBA or XLL UDFs that could hang Excel.
When Excel is done with calculation, it will fire the calculation event again. This is when the results come back in by resolving the promise.
This Script lab gist should give you an indication of how it works:
/*This gist works in combination with any registered Excel JS Custom function*/
$("#set-formulas").click(() => tryCatch(setFormulas));
var rangeToCheck;
async function setFormulas() {
await Excel.run(async (context) => {
//register for event
context.workbook.worksheets.getActiveWorksheet().onCalculated.add(handleCalculate);
//write to grid
const sheet = context.workbook.worksheets.getItem("Sheet1");
rangeToCheck = "A1";
const range = sheet.getRange(rangeToCheck);
range.formulas = [['=CONTOSO.CONTAINS(A1, Days)']];
range.format.autofitColumns();
await context.sync();
});
}
async function handleCalculate(event) {
//read cell
console.log("calc ended - begin");
console.log("Change type of event: " + event.changeType);
console.log("Address of event: " + event.address);
console.log("Source of event: " + event.source);
//Read A1 and log it back to the console
await Excel.run(async (context) => {
//write to grid
const sheet = context.workbook.worksheets.getItem("Sheet1");
const range = sheet.getRange(rangeToCheck);
range.load("values");
await context.sync();
if (range.values.toString() != "GETTING_DATA") {
console.log("Success: " + range.values);
}
});
}
/** Default helper for invoking an action and handling errors. */
async function tryCatch(callback) {
try {
await callback();
} catch (error) {
OfficeHelpers.UI.notify(error);
OfficeHelpers.Utilities.log(error);
}
}

Related

Using Last Row with GetRangeByIndexes - Excel

I'm trying to learn best practices for creating a range in JS. I'm trying to avoid the mistake I did when learning VBA which was that I used the format Range("A1:A2") for a long time before realizing the it was better to use Range(Cells(1,1),Cells(2,1)) because integers are generally easier to work with.
I found getRangeByIndexes for JS, and its not perfect as I have to do math on number of rows if the first row isn't 0. I'd prefer to use integers to set first and last cell in range, but thats another story.
Currently I can select the range if I hard code in the numbers, but now I'm working on adding in the "LastRow" function to make the range dynamic and I can't get it to work. I also struggled to get it to print out to the console.log, but I decided to just try to work with the range vs print it out. I'm currently editing taskpane.js and very new to this.
Here is my code so far:
/*
* Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
* See LICENSE in the project root for license information.
*/
/* global console, document, Excel, Office */
// The initialize function must be run each time a new page is loaded
Office.initialize = () => {
document.getElementById("sideload-msg").style.display = "none";
document.getElementById("app-body").style.display = "flex";
document.getElementById("run").onclick = run;
};
export async function run() {
try {
await Excel.run(async (context) => {
var ws = context.workbook.worksheets.getItem("Sheet1");
ws.activate();
var lrow = ws.getUsedRange().getLastRow();
lrow.load("rowindex");
context.sync();
var range = ws.getRangeByIndexes(0, 0, 4, 5); //This Works
//var range = ws.getRangeByIndexes(0, 0, lrow.rowIndex, 5); //This does nothing, but no errors either, just nothing.
range.select();
await context.sync();
console.log("END");
});
} catch (error) {
console.error(error);
}
}
If I replace lrow.rowindex with 4 it works as expected. Can anyone advise what I'm doing wrong and if this is the best way to generate a range w/ numbers (is there a way to do first/last cell?)
You definitely need the "await" on your first "context.sync()" statment. I added that and it works fine as long as there is data on sheet 1. If lrow.rowIndex evalautes to 0, it will cause an error because getRangesByIndexes needs at least one row to select.
Here's the code as I ran it.
async function run() {
try {
await Excel.run(async (context) => {
var ws = context.workbook.worksheets.getItem("Sheet1");
ws.activate();
var lrow = ws.getUsedRange().getLastRow();
lrow.load("rowindex");
await context.sync();
var range = ws.getRangeByIndexes(0, 0, lrow.rowIndex, 5);
range.select();
await context.sync();
console.log("END");
});
} catch (error) {
console.error(error);
}
}
I took of the "export" directive because the environment I'm working in complained about it.
In case you are interested, I'm using an add-in called the "JavaScript Automation Development Environment (JADE)" from the add-in store. It's meant for automating code in a workbook, not building an add-in, but it is really simple for testing things like this. Disclaimer: I wrote JADE.
I believe you have to call await context.sync() before you can use any properties you load. You're trying to use a loaded property (rowIndex) before the context.sync() call. So that's why I think it's not working.
If you update your code from this:
var lrow = ws.getUsedRange().getLastRow();
lrow.load("rowindex");
var range = ws.getRangeByIndexes(0, 0, 4, 5); //This Works
//var range = ws.getRangeByIndexes(0, 0, lrow.rowIndex, 5); //This does nothing, but no errors either, just nothing.
range.select();
await context.sync();
To this:
var lrow = ws.getUsedRange().getLastRow();
lrow.load("rowindex");
await context.sync();
//var range = ws.getRangeByIndexes(0, 0, 4, 5); //This Works
var range = ws.getRangeByIndexes(0, 0, lrow.rowIndex, 5); //This does nothing, but no errors either, just nothing.
range.select();
await context.sync();
That should fix things.
This is def related to context.sync. I'm still learnign about promise/return/async etc. I switched over to Visual Studio and got a better idea from there project files, this is my adjustment to the default function for the taskpane in there project files.
Note: The nested function finishes off the rest of the function.
function setColor() {
Excel.run(function (context) {
//Start Func
var ws = context.workbook.worksheets.getActiveWorksheet();
var lrow = ws.getUsedRange().getLastRow();
lrow.load("rowindex");
// Run the queued-up command, and return a promise to indicate task completion
return context.sync()
.then(function () {
//var range = ws.getRangeByIndexes(0, 0, 4, 5); //This Works
var range = ws.getRangeByIndexes(0, 0, lrow.rowIndex, 5); //This does nothing, but no errors either, just nothing.
range.select();
})
//End Func
return context.sync();
}).catch(function (error) {
console.log("Error: " + error);
if (error instanceof OfficeExtension.Error) {
console.log("Debug info: " + JSON.stringify(error.debugInfo));
}
});
}
})();

IndexedDB's callbacks not being executed inside the 'fetch' event of a Service Worker

I'm trying to do a couple of things in the IndexedDB database inside the 'fetch' event of a service worker, when the aplication asks the server for a new page. Here's what I'm going for:
Create a new object store (they need to be created dynamically, according to the data that 'fetch' picks up);
Store an element on the store.
Or, if the store already exists:
Get an element from the store;
Update the element and store it back on the store.
The problem is that the callbacks (onupgradeneeded, onsuccess, etc) never get executed.
I've been trying with the callbacks inside of each other, though I know that may not be the best approach. I've also tried placing an event.waitUntil() on 'fetch' but it didn't help.
The 'fetch' event, where the function registerPageAccess is called:
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
event.waitUntil(function () {
const nextPageURL = new URL(event.request.url);
if (event.request.destination == 'document') {
if (currentURL) {
registerPageAccess(currentURL, nextPageURL);
}
currentURL = nextPageURL;
}
}());
/*
* some other operations
*/
return response || fetch(event.request);
})
);
});
registerPageAccess, the function with the callbacks.
I know it's plenty of code, but just look at secondRequest.onupgradeneeded in the 5th line. It is never executed, let alone the following ones.
function registerPageAccess(currentPageURL, nextPageURL) {
var newVersion = parseInt(db.version) + 1;
var secondRequest = indexedDB.open(DB_NAME, newVersion);
secondRequest.onupgradeneeded = function (e) {
db = e.target.result;
db.createObjectStore(currentPageURL, { keyPath: "pageURL" });
var transaction = request.result.transaction([currentPageURL], 'readwrite');
var store = transaction.objectStore(currentPageURL);
var getRequest = store.get(nextPageURL);
getRequest.onsuccess = function (event) {
var obj = getRequest.result;
if (!obj) {
// Insert element into the database
console.debug('ServiceWorker: No matching object in the database');
const addRes = putInObjectStore(nextPageURL, 1, store);
addRes.onsuccess = function (event) {
console.debug('ServiceWorker: Element was successfully added in the Object Store');
}
addRes.onerror = function (event) {
console.error('ServiceWorker error adding element to the Object Store: ' + addRes.error);
}
}
else {
// Updating database element
const updRes = putInObjectStore(obj.pageURL, obj.nVisits + 1, store);
updRes.onsuccess = function (event) {
console.debug('ServiceWorker: Element was successfully updated in the Object Store');
}
updRes.onerror = function (event) {
console.error('ServiceWorker error updating element of the Object Store: ' + putRes.error);
}
}
};
};
secondRequest.onsuccess = function (e) {
console.log('ServiceWorker: secondRequest onsuccess');
};
secondRequest.onerror = function (e) {
console.error('ServiceWorker: error on the secondRequest.open: ' + secondRequest.error);
};
}
I need a way to perform the operations in registerPageAccess, which involve executing a couple of callbacks, but the browser seems to kill the Service Worker before they get to occur.
All asynchronous logic inside of a service worker needs to be promise-based. Because IndexedDB is callback-based, you're going to find yourself needing to wrap the relevant callbacks in a promise.
I'd strongly recommend not attempting to do this on your own, and instead using one of the following libraries, which are well-tested, efficient, and lightweight:
idb-keyval, if you're okay with a simple key-value store.
idb if you're need the full IndexedDB API.
I'd also recommend that you consider using the async/await syntax inside of your service worker's fetch handler, as it tends to make promise-based code more readable.
Put together, this would look roughly like:
self.addEventListener('fetch', (event) => {
event.waitUntil((async () => {
// Your IDB cleanup logic here.
// Basically, anything that can execute separately
// from response generation.
})());
event.respondWith((async () => {
// Your response generation logic here.
// Return a Response object at the end of the function.
})());
});

Get Header's OOXML

I am unable to get the OOXML of a Header. According to the documentation getHeader" method will return Body type. The Body has a method to get OOXML. But it looks like it is not returning the OOXML. Maybe I am missing something?
Here's my code:
Word.run(function (context) {
// Create a proxy sectionsCollection object.
var mySections = context.document.sections;
// Queue a commmand to load the sections.
context.load(mySections, 'body/style');
// Synchronize the document state by executing the queued commands,
// and return a promise to indicate task completion.
return context.sync().then(function () {
// header
var headerBody = mySections.items[0].getHeader("primary");
// header OOXML
//// NOT GETTING OOXML HERE
var headerOOXML = headerBody.getOoxml();
// Synchronize the document state by executing the queued commands,
// and return a promise to indicate task completion.
return context.sync().then(function () {
// modify header
var headerOOXMLValue = ModifyHeaderMethod(headerOOXML.value);
headerBody.clear();
headerBody.insertOoxml(headerOOXMLValue, 'Start');
// Synchronize the document state by executing the queued commands,
// and return a promise to indicate task completion.
return context.sync().then(function () {
callBackFunc({
isError: false
});
});
});
});
})
The "art" of Office.js is to minimize the number of "syncs" you do. I know that is kind of an unnecessary burden, but that's how it is.
With that in mind, In this case you only need ONE sync.
this code works (assuming that you have only one section in the doc).
btw you can try it in script lab with this yaml.
if this does not work, please indicate if this is Word for Windows (and what build) or Online, or Mac... thanks!
async function run() {
await Word.run(async (context) => {
let myOOXML = context.document.sections.getFirst()
.getHeader("primary").getOoxml();
await context.sync();
console.log(myOOXML.value);
});
}
You have a lot of extra code here but the gist of your problem is that headerOOXML won't be populated until you sync():
Word.run(function (context) {
var header = context.document.sections // Grabv
.getFirst() // Get the first section
.getHeader("primary"); // Get the header
var ooxml = header.getOoxml();
return context.sync().then(function () {
console.log(ooxml.value);
});
});

Google Script: Play Sound when a specific cell change the Value

Situation:
Example Spreadsheet
Sheet: Support
Column: H has the following function "=IF(D:D>0;IF($B$1>=$G:G;"Call";"In Time");" ")" that changes the value depending on the result.
Problem:
I need to:
Play a sound when a cell in column H changes to "Call" on the sheet "Support".
This function will need to run every 5min.
Does the sound need to be uploaded to Drive or can I use a sound from a URL?
I will appreciate to anyone can help on it... I see a lot of code but I didn't understand very well.
This is a pretty tough problem, but it can be done with a sidebar that periodically polls the H column for changes.
Code.gs
// creates a custom menu when the spreadsheet is opened
function onOpen() {
var ui = SpreadsheetApp.getUi()
.createMenu('Call App')
.addItem('Open Call Notifier', 'openCallNotifier')
.addToUi();
// you could also open the call notifier sidebar when the spreadsheet opens
// if you find that more convenient
// openCallNotifier();
}
// opens the sidebar app
function openCallNotifier() {
// get the html from the file called "Page.html"
var html = HtmlService.createHtmlOutputFromFile('Page')
.setTitle("Call Notifier");
// open the sidebar
SpreadsheetApp.getUi()
.showSidebar(html);
}
// returns a list of values in column H
function getColumnH() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Support");
// get the values in column H and turn the rows into a single values
return sheet.getRange(1, 8, sheet.getLastRow(), 1).getValues().map(function (row) { return row[0]; });
}
Page.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<p id="message">Checking for calls...</p>
<audio id="call">
<source src="||a URL is best here||" type="audio/mp3">
Your browser does not support the audio element.
</audio>
<script>
var lastTime = []; // store the last result to track changes
function checkCalls() {
// This calls the "getColumnH" function on the server
// Then it waits for the results
// When it gets the results back from the server,
// it calls the callback function passed into withSuccessHandler
google.script.run.withSuccessHandler(function (columnH) {
for (var i = 0; i < columnH.length; i++) {
// if there's a difference and it's a call, notify the user
if (lastTime[i] !== columnH[i] && columnH[i] === "Call") {
notify();
}
}
// store results for next time
lastTime = columnH;
console.log(lastTime);
// poll again in x miliseconds
var x = 1000; // 1 second
window.setTimeout(checkCalls, x);
}).getColumnH();
}
function notify() {
document.getElementById("call").play();
}
window.onload = function () {
checkCalls();
}
</script>
</body>
</html>
Some sources to help:
Sidebars and Dialogs
Custom Menus
Simple Trigger - onOpen
`google.script.run.withSuccessHandler(callback).customFunction()
Array.prototype.map
Recursively calling checkCalls() eventually led to errors, when I implemented the main answer given (which is mostly correct and really useful, so thank you!).
// Note: But the original implementation would work fine for a while - say 90 minutes - then crash. The call that would normally take 1 second would take 300 seconds, and Execution would Halt. It looks like it blew the stack by keeping on recursively calling itself. When moved to a single call of check() with proper exiting of the function, it then worked.
The console log in Chrome on running the JavaScript, said this:
ERR_QUIC_PROTOCOL_ERROR.QUIC_TOO_MANY_RTOS 200
After much investigation, I worked out a better way of doing it... Which doesn't require recursion (and therefore won't blow the stack).
Remove this line:
// window.setTimeout(checkCalls, 500);
And use something like this - at the end of your script:
// This function returns a Promise that resolves after "ms" Milliseconds
// The current best practice is to create a Promise...
function timer(ms) {
return new Promise(res => setTimeout(res, ms));
}
async function loopthis () { // We need to wrap the loop into an async function for the await call (to the Promise) to work. [From web: "An async function is a function declared with the async keyword. Async functions are instances of the AsyncFunction constructor, and the await keyword is permitted within them. The async and await keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains."]
for (var i = 0; i >= 0; i++) {
console.log('Number of times function has been run: ' + i);
checkCalls();
await timer(3000);
}
}
window.onload = function () {
loopthis();
}
</script>

How to read Object from Firebase using AngularJS

I am developing my app, and one of the features will be messaging within the application. What I did, is I've developed 'send message' window, where user can send message to other user. The logic behind it is as following:
1. User A sends message to User B.
2. Firebase creates following nodes in 'Messaging':
"Messaging"->"User A"->"User B"->"Date & Time"->"UserA: Message"
"Messaging"->"User B"->"User A"->"Date & Time"->"UserA: Message"
Here is the code that I am using for sending messages:
sendMsg: function(receiver, content) {
var user = Auth.getUser();
var sender = user.facebook.id;
var receiverId = receiver;
var receiverRef = $firebase(XXX.firebase.child("Messaging").child(receiverId).child(sender).child(Date()));
var senderRef = $firebase(XXX.firebase.child("Messaging").child(sender).child(receiverId).child(Date()));
receiverRef.$set(sender,content);
senderRef.$set(sender,content);
},
(picture 1 in imgur album)
At the moment, I am trying to read the messages from the database, and sort them in according to date. What I've accomplished so far, is that I have stored the content of "Messaging/UserA/" in form of an Object. The object could be seen in the picture I've attached (picture 2).
http://imgur.com/a/3zQ0o
Code for data receiving:
getMsgs: function () {
var user = Auth.getUser();
var userId = user.facebook.id;
var messagesPath = new Firebase("https://xxx.firebaseio.com/Messaging/");
var Messages = messagesPath.child(userId);
Messages.on("value", function (snapshot) {
var messagesObj = snapshot.val();
return messagesObj;
}, function (errorObject) {
console.log("Error code: " + errorObject.code);
});
}
My question is: how can I read the object's messages? I would like to sort the according to the date, get the message and get the Id of user who has sent the message.
Thank you so much!
You seem to be falling for the asynchronous loading trap when you're reading the messages:
getMsgs: function () {
var user = Auth.getUser();
var userId = user.facebook.id;
var messagesPath = new Firebase("https://xxx.firebaseio.com/Messaging/");
var Messages = messagesPath.child(userId);
Messages.on("value", function (snapshot) {
var messagesObj = snapshot.val();
return messagesObj;
}, function (errorObject) {
console.log("Error code: " + errorObject.code);
});
}
That return statement that you have in the Messages.on("value" callback doesn't return that value to anyone.
It's often a bit easier to see what is going on, if we split the callback off into a separate function:
onMessagesChanged(snapshot) {
// when we get here, either the messages have initially loaded
// OR there has been a change in the messages
console.log('Inside on-value listener');
var messagesObj = snapshot.val();
return messagesObj;
},
getMsgs: function () {
var user = Auth.getUser();
var userId = user.facebook.id;
var messagesPath = new Firebase("https://xxx.firebaseio.com/Messaging/");
var Messages = messagesPath.child(userId);
console.log('Before adding on-value listener');
Messages.on("value", onMessagesChanged);
console.log('After adding on-value listener');
}
If you run the snippet like this, you will see that the console logs:
Before adding on-value listener
After adding on-value listener
Inside on-value listener
This is probably not what you expected and is caused by the fact that Firebase has to retrieve the messages from its servers, which could potentially take a long time. Instead of making the user wait, the browser continues executing the code and calls your so-called callback function whenever the data is available.
In the case of Firebase your function may actually be called many times, whenever a users changes or adds a message. So the output more likely will be:
Before adding on-value listener
After adding on-value listener
Inside on-value listener
Inside on-value listener
Inside on-value listener
...
Because the callback function is triggered asynchronously, you cannot return a value to the original function from it. The simplest way to work around this problem is to perform the update of your screens inside the callback. So say you want to log the messages, you'd do:
onMessagesChanged(snapshot) {
// when we get here, either the messages have initially loaded
// OR there has been a change in the messages
console.log('Inside on-value listener');
var i = 0;
snapshot.forEach(function(messageSnapshot) {
console.log((i++)+': '+messageSnapshot.val());
});
},
Note that this problem is the same no matter what API you use to access Firebase. But the different libraries handle it in different ways. For example: AngularFire shields you from a lot of these complexities, by notifying AngularJS of the data changes for you when it gets back.
Also see: Asynchronous access to an array in Firebase

Categories