How to simplify transition from Apps Script libraries to AddOn? - google-apps-script

When developing Google Apps Scripts, I make use of libraries since there's lots of shared code. If I have a library A:
function foo() { ... }
Then if I use that library in another script, B, the foo function is exposed via a library resource and gets called like:
function bar() {
A.foo();
...
}
However, the Google AddOn documentation says to never use libraries. So I prep the AddOn by combining all the library files, which puts everything in the same context, and now the same call from bar should be foo() and not A.foo().
I've been trying to think of some trick or way of doing all this that will allow me to combine the files without going through and rewriting all the library function calls (e.g., find 'A.' replace '') or transferring all the functions to an object (e.g., A = { foo: function() {...} }). I'd like to be able to just copy and paste all the library bits, but I can't figure (or find) a way to do it.

Does this little example help? There might be a better/easier way to do it, but it works with minimal editing of the original library. The other common method of exposing a library's functions don't seem to work with apps script.
Option 1:
var a = new A();
function main() {
Logger.log(a.foo());
}
function A() {
var self = this;
self.foo = function() {
return "Hello, world!";
}
function bar() {
return "I'm private!";
}
}
Option 2:
A little more complicated using prototype...
function main() {
var a = new A();
Logger.log(a.foo());
}
var A = function() {
var self = this;
self.bar = function() {
return "World!";
}
}
A.prototype.foo = function() { //Prototype public stuff
return "Hello, " + this.bar();
}
Either way they will have to modified a bit. Does anybody else know a better way?

Related

Load template from script level using HtmlService.createHtmlOutputFromFile call via a Google Apps Script library

I have the following function in a library called lib.
var Template =
{
GetContents: function (templateName)
{
var templateContents;
try
{
templateContents = HtmlService.createHtmlOutputFromFile(templateName).getContent();
}
catch (e)
{
return undefined;
}
return templateContents;
},
};
I would like to call this function from a script that uses the library, like so:
templateContents = lib.Template.GetContents('htmltemplate');
However, when I do this it does not return 'htmltemplate' from the script level - it returns 'htmltemplate' from the library level instead. I Would like to retrieve the contents of 'htmltemplate' from the script level, not the library level, how can I get the desired result?

How to make the library work with the caller script PropertiesService?

Until Google extends the import/export API to container-bound Apps Script projects, I have moved most of my project to a library which can use that API, and then made Google Docs project into a shell that just calls through to the library.
My problem is having the library access the same properties (PropertiesService) as the Google Doc project. Since I have existing users of my Docs Add-on, I need to keep using these properties.
In my Google Doc project, I tried
$.PropertiesService = PropertiesService;
(where $ is my library).
It didn't work. The library kept using its own properties.
So then I tried:
function _mock(obj) {
var ret = {};
for(var key in obj) {
if(typeof obj[key] == 'function') {
ret[key] = obj[key].bind(obj);
} else {
ret[key] = obj[key];
}
}
return ret;
}
$.PropertiesService = _mock(PropertiesService);
Still not working. Trying again:
function _mock(obj) {
var ret = {};
for(var key in obj) {
if(typeof obj[key] == 'function') {
ret[key] = (function(val) {
return function() {
return val.apply(obj, arguments);
};
})(obj[key]);
} else {
ret[key] = obj[key];
}
}
return ret;
}
$.PropertiesService = _mock(PropertiesService);
This works.
At this point, I'm wondering:
Why did the first two ways not work, but the third way did?
Can I expect this to continue working?
Is there a better way to have a library access the main script's properties?
Documentation is sparse. There is this, but the PropertiesService is not mentioned.
Sharing of resources
As you are aware, libraries have shared and non-shared resources. PropertiesService is listed under non-shared resources, meaning that the library has its own instance of the service that is accessed when you reference it in the library code.
const getStore = () => PropertiesService.getScriptProperties();
If the function above is declared in the library, it will use the library's resource, if in the calling script - its own instance.
V8 runtime solution
V8 runtime does not create a special context for your code and gives you access to built-in services directly. Because of this when using the runtime, the service can be injected by simply defining or replacing a property on a global this:
//in the library;
var getProperty = ((ctxt) => (key) => {
var service = ctxt.injectedService;
var store = service.getScriptProperties();
return store.getProperty(key);
})(this);
var setProperty = ((ctxt) => (key, val) => {
var service = ctxt.injectedService;
var store = service.getScriptProperties();
return store.setProperty(key, val);
})(this);
var inject = ((ctxt) => (service) => ctxt.injectedService = service)(this);
var greet = ((ctxt) => () => {
var store = ctxt.injectedService.getScriptProperties();
return store.getProperty("greeting") || "Ola!";
})(this);
//in the calling script;
function testSharedResources() {
PropertiesService.getScriptProperties().setProperty("greeting", "Hello, lib!");
$.inject(PropertiesService);
Logger.log($.greet()); //Hello, lib!
$.setProperty("greeting", "Hello, world!");
Logger.log($.greet()); //Hello, world!
}
In some contexts global this will be undefined (I encountered this when adding a library to a bound script). In this case, simply define a private global namespace (to avoid leaking to the caller script):
//in the library;
var Dependencies_ = {
properties : PropertiesService
};
var use = (service) => {
if ("getScriptProperties" in service) {
Dependencies_.properties = service;
}
};
//in the calling script;
$.use(PropertiesService);
Rhino runtime solution
Older Rhino runtime, on the other hand, creates a special implicit context. This means that you have no access to built-in services or the global this. Your only option is to bypass calling the service in the library (your approach #3 is perfect for doing so).
Questions
Why did the first two ways not work, but the third way did?
All issues with your approaches boil down to:
Resource sharing (libraries have their own service instances)
Special implicit context (no external access to lib built-ins in Rhino)
But there is a catch: all 3 approaches do work as designed.
First, Approach one does work if you specifically reference the PropertiesService on $. This makes sense as the library is included as a namespace with members mapped to global declarations in the library. For example:
//in the caller script
PropertiesService.getScriptProperties().setProperty("test", "test");
$.PropertiesService = PropertiesService;
Logger.log( $.PropertiesService.getScriptProperties().getProperty("test") ); // "test"
Logger.log( $.getProperty("test") ); // "null"
//in the library
function getProperty(key) {
var store = PropertiesService.getScriptProperties();
return store.getProperty(key);
}
Approach two also works. Binding of the function in the caller script does not change the fact if called in the library it receives library context, but if you call the bound copy directly in the calling script, it works:
//in the caller script
PropertiesService.getScriptProperties().setProperty("test", "test");
var bound = $.PropertiesService.getScriptProperties.bind(PropertiesService);
var obj = { getScriptProperties : bound };
$.PropertiesService = obj;
Logger.log( bound().getProperty("test") ); // "test"
Logger.log( $.getProperty("test") ); // "null"
Now, why does the third approach work out of the box? Because of the closure resulting from the wrapped function capturing the PropertiesService of the calling script and applying the getScriptProperties method. To illustrate:
//in the caller script
var appl = {
getScriptProperties : (function(val) {
return function() {
return val.apply(PropertiesService);
};
})(PropertiesService.getScriptProperties)
};
$.PropertiesService = appl;
Logger.log( $.getProperty("test") ); // "test"
Can I expect this to continue working?
Yes and no. Yes, because your _mock function behavior exhibits the expected behavior in all cases. No, because apply relies on the getScriptProperties not being implemented as an arrow function where this override will be ignored.
Is there a better way to have library access the main script's properties?
For Rhino runtime - don't think so. For V8 - direct injection of the service will suffice.

requestAnimationFrame type error in object

hi i'm trying to use requestAnimationFrame for my game and I actually use this code below, but as you can see ".bind()" create every loop a new function that slow down my game... I'm looking for a "efficient" solution for the best perfomance, thank you in advance :D
function myClass() {
this.loop = function() {
window.requestAnimationFrame(this.loop.bind(this));
/* here myGameLoop */
}
this.loop();
}
above code works but is slow.. instead this "standard" code give me "Type error":
window.requestAnimationFrame(this);
I have also found e tried this Q&A: requestAnimationFrame attached to App object not Window works just ONE time then give the same "Type error" :(
try if you don't believe me: http://jsfiddle.net/ygree/1 :'(
Without knowing the whole story of your object (what else is in there); you could simplify life by just doing this:
function myClass() {
var iHavAccessToThis = 1;
function loop() {
iHavAccessToThis++;
/* here myGameLoop */
requestAnimationFrame(loop);
}
loop();
//if you need a method to start externally use this instead of the above line
this.start = function() { loop() }
//...
return this;
}
Now you don't need to bind anything and you access local scope which is fast.
And then call:
var class1 = new myClass();
class1.start();

AS3 Passing Variable Parameters to a generic Function Menu / SubItems

I'm no code genius, but a fan of action script.
Can you help me on this:
I have a function that depending on the object selected, will call event listeners to a set of 'sub-items' that are already on stage (I want to reuse this subitems with changed parameters upon click, instead of creating several instances and several code).
So for each selected 'case' I have to pass diferent variables to those 'sub-items', like this:
function fooMenu(event:MouseEvent):void {
switch (event.currentTarget.name)
{
case "btUa1" :
trace(event.currentTarget.name);
// a bunch of code goes here
//(just cleaned to easy the view)
/*
HELP HERE <--
here is a way to pass the variables to those subitems
*/
break;
}
}
function fooSub(event:MouseEvent):void
{
trace(event.target.data);
trace(event.currentTarget.name);
// HELP PLEASE <-> How can I access the variables that I need here ?
}
btUa1.addEventListener(MouseEvent.CLICK, fooMenu);
btUa2.addEventListener(MouseEvent.CLICK, fooMenu);
btTextos.addEventListener(MouseEvent.CLICK, fooSub);
btLegislacao.addEventListener(MouseEvent.CLICK, fooSub);
Anyone to help me please?
Thank very much in advance. :)
(I'm not sure I got your question right, and I haven't developed in AS3 for a while.)
If you want to simply create function with parameters which will be called upon a click (or other event) you can simply use this:
btUa1.addEventListener(MouseEvent.CLICK, function() {
fooMenu(parameters);
});
btUa2.addEventListener(MouseEvent.CLICK, function() {
fooMenu(other_parameters)
}):
public function fooMenu(...rest):void {
for(var i:uint = 0; i < rest.length; i++)
{
// creating elements
}
}
If you want to call event listeners assigned to something else you can use DispatchEvent
btnTextos.dispatchEvent(new MouseEvent(MouseEvent.CLICK))
Remember, you can't use btTextos.addEventListener(MouseEvent.CLICK, carregaConteudo("jocasta")); because the 2nd parameter you pass while adding Eventlistener will be considered as function itself - there are two proper ways to use addEventListener:
1:
function doSomething(event:MouseEvent):void
{
// function code
}
element.addEventListener(MouseEvent.CLICK, doSomething); //notice no brackets
2:
element.addEventListener(MouseEvent.CLICK, function() { // function code });
So:
function fooSub(event:MouseEvent, bla:String):void
{
trace(event.currentTarget.name+" - "+bla);
// bla would be a clip name.
}
codebtTextos.addEventListener(MouseEvent.CLICK, function(e:MouseEvent) { fooSub(e, "jocasta") } );
Or try something like this if you want content to be dynamically generated:
btUa1.addEventListener(MouseEvent.CLICK, function() {
createMenu(1);
});
btUa2.addEventListener(MouseEvent.CLICK, function() {
createMenu(2);
});
function createMenu(id):void
{
// Switching submenu elements
switch (id)
{
case 1:
createSubmenu([myFunc1, myFunc2, myFunc3]); // dynamically creating submenus in case you need more of them than u already have
break;
case 2:
createSubmenu([myFunc4, myFunc5, myFunc6, myFunc7]);
break;
default:
[ and so on ..]
}
}
function createSubmenu(...rest):void {
for (var i:uint = 0; i < rest.length; i++)
{
var mc:SubItem = new SubItem(); // Subitem should be an MovieClip in library exported for ActionScript
mc.addEventListener(MouseEvent.CLICK, rest[i] as function)
mc.x = i * 100;
mc.y = 0;
this.addChild(mc);
}
}
Your question is rather vague; what "variables" do you want to "pass"? And what do you mean by "passing the variable to a sub item"? Usually "passing" means invoking a function.
If you can be more specific on what exactly your trying to do that would be helpful. In the meantime, here are three things that may get at what you want:
You can get any member of any object using bracket notation.
var mc:MovieClip = someMovieClip;
var xVal:Number = mc.x; // The obvious way
xVal = mc["x"]; // This works too
var propName:String = "x";
xVal = mc[propName] ; // So does this.
You can refer to functions using variables
function echo(message:String):void {
trace(message);
}
echo("Hello"); // The normal way
var f:Function = echo;
f("Hello"); // This also works
You can call a function with all the arguments in an array using function.apply
// Extending the above example...
var fArgs:Array = ["Hello"];
f.apply(fArgs); // This does the same thing
Between these three things (and the rest parameter noted by another poster) you can write some very flexible code. Dynamic code comes at a performance cost for sure, but as long as the frequency of calls is a few hundred times per second or less you'll never notice the difference.

Is there a work around for an object oriented front-end in Google Script

I'm having some problems with an event handler in the object below. I can't remember the error message but it basically said that it could not find the function. The code below is an example of what I'm trying to do.
var anObject = function () {
var n = 0;
var HandleClick(e) {
n ++;
};
return {
Init: function () {
var app = UiApp.getActiveApplication();
var handler = app.createServerHandler("HandleClick");
var com = UiApp.LoadComponent("MyGui", {prefix: "a"});
com.getElementById("button").addClickHandler(handler);
}
}
}
Would really appreciate a work-around if possible, if that is not possible then please tell me what you would suggest because I'm not sure how best to get around this.
Thanks guys.
All handler functions must be top level functions on your script. It's not possible to have it inside an object like this.