I want to invoke custom methods on Google Apps Script classes such as Spreadsheet, Sheet, and DriveApp.
In https://stackoverflow.com/a/6117889, a minimal prototyping solution is used to add a method to the Javascript Date() class to get the week number. Is it possible to apply the same strategy to Google Apps Script classes?
As an example, I want to create a custom method for the Spreadsheet class that allows the spreadsheet to be moved to a particular folder in my google drive given the ID of that folder. Here is what I've tried:
Spreadsheet.prototype.moveToFolder = function(folderID) {
const file = DriveApp.getFileById(this.getId());
const destination = DriveApp.getFolderById(folderID);
file.moveTo(destination);
}
However, I get the error message "ReferenceError: Spreadsheet is not defined".
Is there another way to achieve what I want?
It is possible to add custom methods. But Spreadsheet class is not directly accessible. So, it is needed to first get a instance of Spreadsheet class using any of methods available:
const Spreadsheet = SpreadsheetApp.getActive();
Then use Object.getPrototypeOf() on the Spreadsheet instance to get it's prototype.
Object.getPrototypeOf(Spreadsheet).myMethod = function (){
console.info("myMethod was called!")
return true;
}
Any property defined on the prototype will then propogate through all Spreadsheet instances.
Update:
The prototype object returned by Object.getPrototypeOf(Spreadsheet) is Object. This can also be confirmed by logging Spreadsheet.constructor.name. This means that there is no special Spreadsheet prototype or constructor used to create the Spreadsheet instance. Therefore, Although you can add custom methods, They're added to all objects say, Range, DriveApp and any object created with var obj = {} or Object.create("Any object except null").
Given that Spreadsheet doesn't have a unique prototype but actually uses that of Object, as noted by TheMaster, you can simply add your method to the Object prototype.
Object.prototype.moveToFolder = function(folderID) {
const file = DriveApp.getFileById(this.getId());
const destination = DriveApp.getFolderById(folderID);
file.moveTo(destination);
}
Since this method will apply to all objects, you should ask yourself if it's really worth doing. See "Why is extending native objects a bad practice?"
Instead of modifying the native object, you could create a new class that "inherits" the native methods while also giving you the ability to override and add new methods.
function main() {
const ss = new Spreadsheet(SpreadsheetApp.getActive());
console.log(ss._native.getName()); // MySpreadsheet
console.log(ss.getName()); // The name is MySpreadsheet
ss.moveToFolder(FOLDER_ID);
}
class Spreadsheet {
constructor(native) {
this._native = native;
// Copy native's methods to this
Object.getOwnPropertyNames(this._native).forEach(property => {
this[property] = this._native[property];
});
// Override native methods
Object.defineProperties(this, {
'getName': {
value: function() {
return `The name is ${this._native.getName()}`;
}
}
});
}
moveToFolder(folderId) {
const file = DriveApp.getFileById(this.getId());
const destination = DriveApp.getFolderById(folderId);
file.moveTo(destination);
}
}
Related
I have a custom string prototype that does some actions to a string;
String.prototype.norm_to_ascii=function(){return unescape(encodeURIComponent(this))};
In my example, the string that I want to apply the prototype to is a global object property that lives outside of the SampleObject object. In my actual code it would be referenced like this;
var userObject = {
name: "SomeName",
id: "SomeID"
}
It works everywhere in my project (other js files) except for within a particular Object method;
var SampleObject = { //This is in it's own js file called sampleobject.js
test: 0,
doStringThings {
let something = userObject.id.norm_to_ascii() //RETURNS userObject.id.norm_to_ascii is not a function
}
}
So in the SampleObject, I need to use the id, for example, but I need to do some basic decoding of the id value that is in the userObject which is what the string prototype does.
I can use this string prototype elsewhere. This is in a chrome extension so I have defined the prototype in the service worker and it can be used in the popup and content pages as well as the service worker so it must have to do with the object method but I can't figure out why?
Can anyone offer any suggestions to expose that prototype to the object method without having to redefine it?
EDIT
I should have been more clear in my explanation. I updated my example above.
You forget about this
this.otherTestValue.norm_to_ascii()
After seeing the updated question my conclusion is that you are defining the norm_to_ascii function after you run it.
Changing the order of the imports should fix the problem. Can you show us the structure of the project and where are you importing the file with that prototype?
The question is related to general js programming, but I'll use nightwatch.js as an example to elaborate my query.
NightWatch JS provides various chaining methods for its browser components, like: -
browser
.setValue('input[name='email']','example#mail.com')
.setValue('input[name='password']', '123456')
.click('#submitButton')
But if I'm writing method to select an option from dropdown, it requires multiple steps, and if there are multiple dropdowns in a form, it gets really confusing, like: -
browser
.click(`#country`)
.waitForElementVisible(`#india`)
.click(`#india`)
.click(`#state`)
.waitForElementVisible(`#delhi`)
.click(`#delhi`)
Is it possible to create a custom chaining method to group these already defined methods? For example something like:
/* custom method */
const dropdownSelector = (id, value) {
return this
.click(`${id}`).
.waitForElementVisible(`${value}`)
.click(`${value}`)
}
/* So it can be used as a chaining method */
browser
.dropdownSelector('country', 'india')
.dropdownSelector('state', 'delhi')
Or is there any other way I can solve my problem of increasing reusability and readability of my code?
I'm somewhat new to JS so couldn't tell you an ideal code solution, would have to admit I don't know what a proxy is in this context. But in the world of Nightwatch and test-automation i'd normally wrap multiple steps I plan on reusing into a page object. Create a new file in a pageObject folder and fill it with the method you want to reuse
So your test...
browser
.click(`#country`)
.waitForElementVisible(`#india`)
.click(`#india`)
.click(`#state`)
.waitForElementVisible(`#delhi`)
.click(`#delhi`)
becomes a page object method in another file called 'myObject' like...
selectLocation(browser, country, state, city) {
browser
.click(`#country`) <== assume this never changes?
.waitForElementVisible(country)
.click(country)
.click(state)
.waitForElementVisible(city)
.click(city);
}
and then each of your tests inherit the method and define those values themselves, however you chose to manage that...
const myObject = require ('<path to the new pageObject file>')
module.exports = {
'someTest': function (browser) {
const country = 'something'
const state = 'something'
const city = 'something'
myObject.selectLocation(browser);
You can also set your country / state / city as variables in a globals file and set them as same for everything but I don't know how granular you want to be.
Hope that made some sense :)
This is a great place to use a Proxy. Given some class:
function Apple ()
{
this.eat = function ()
{
console.log("I was eaten!");
return this;
}
this.nomnom = function ()
{
console.log("Nom nom!");
return this;
}
}
And a set of "extension methods":
const appleExtensions =
{
eatAndNomnom ()
{
this.eat().nomnom();
return this;
}
}
We can create function which returns a Proxy to select which properties are retrieved from the extension object and which are retrieved from the originating object:
function makeExtendedTarget(target, extensions)
{
return new Proxy(target,
{
get (obj, prop)
{
if (prop in extensions)
{
return extensions[prop];
}
return obj[prop];
}
});
}
And we can use it like so:
let apple = makeExtendedTarget(new Apple(), appleExtensions);
apple
.eatAndNomnom()
.eat();
// => "I was eaten!"
// "Nom nom!"
// "I was eaten!"
Of course, this requires you to call makeExtendedTarget whenever you want to create a new Apple. However, I would consider this a plus, as it makes it abundantly clear you are created an extended object, and to expect to be able to call methods not normally available on the class API.
Of course, whether or not you should be doing this is an entirely different discussion!
I am writing an app that has features that can be turned on and off via a config.json that looks something like this:
"appFeatures": {
"header": {
"create": true,
"title": "Here Comes the Sun"
},
"imageStrip": {
"create": false,
"imageDirectory": "../data/images",
"imageDimensions": [288, 162]
},
"areaChart": {
"create": true
},
"axes": {
"create": true
}
}
For each feature there is already a corresponding class of the same name that implements the feature. I'd like to use the name of the feature to create a new instance of the class. After fetching the config.json, I have code (within a Main.js class) that looks like:
this.features = Object.entries(this.config.appFeatures)
.filter((entry) => {
return entry[1].create === true;
});
this.features.forEach((feature) => { this.createFeatureInstances(feature[0]); });
And then I try to create instances, a la this.header = new Header():
createFeatureInstances(featureName) {
const className = `${featureName.replace(featureName[0], featureName[0].toUpperCase())}`;
this[`${featureName}`] = new Function(`
return class ${className} {}
`)();
This creates a new, empty Header class and, I suppose, it's instance. It is not the Header class that I have already written and want to create an instance for. How might I write the createFeatureInstances function so that I can create the instance of each class that corresponds to a feature?
EDIT Because new features may be added to this app in the future by others, I would like to minimize the times that I hard code which features are available to the app. With my current design, another developer can add another feature by writing a new feature class, importing that class into the Main.js class, and pop the config entries into the config .json without having to touch anything else in the code. For this reason, solutions like this one: Create an instance of a class in ES6 with a dynamic name? won't give me a solution because they rely on having a complete list of the classes that should already exist.
You need to have a name-class mapping somewhere. Factory function or somewhere else is your call. Your current solution lacks this, which is causing the problem:
...creates a new, empty Header class and, I suppose, it's instance. It is not the Header class that I have already written and want to create an instance for
Some explanation with a simple example
// Imagine you've defined Test
class Test {
callMe() {}
}
// And your consumer wants a className Test to be created
const className = "Test";
// This is a simple map of the name to class
const nameToClass = {
"Test": Test
}
// This creates a new class called Test, not your defined class
const AttemptedDynamicTest = new Function(`
return class ${className} {}
`)();
// The next two logs just prove the previous comment
console.log(Test === AttemptedDynamicTest); // false
console.log(typeof AttemptedDynamicTest.prototype.callMe); // undefined
// What you really need is to maintain a map, and just use it to dynamically
// create a new instance every time
console.log(typeof nameToClass[className].prototype.callMe); // function
You can use a string to initialize a cooresponding (valid and existing) class using the following snippet:
var dynamicClassInstance = new this[classNameString]();
Just trying to wrap my head around prototype-based design
Problem: implement a data structure say priority-queue with a known API. Instantiate multiple instances of the PQ.
So I used the revealing module pattern as follows
module.exports = (function () {
// ... assume the following methods are revealed. Other private methods/fields are hidden
let priorityQueue = {
insert,
removeMax,
isEmpty,
toString
};
return {
priorityQueue,
newObj: (comparer, swapper) => {
let instance = Object.create(priorityQueue);
instance.array = [];
instance.size = 0;
instance.less = comparer;
instance.swap = swapper;
return instance;
}
}
})();
Created a newObj factory method to create valid instances. priorityQueue is the API/prototype.
So methods belong in the prototype.
Instance Fields cannot reside there ; they would be shared across instances.
However in this case, the internal fields of the PQ are not encapsulated.
const pQ = require('./priorityQueue').newObj(less, swap);
pQ.array = undefined; // NOOOOOOO!!!!
Update: To clarify my question, the methods in the prototype object need to operate on the instance fields array & size. However these fields cannot be shared across instances. How would the methods in the prototype close over instance fields in the object?
Don't assign array or whatever you want to encapsulate to new object.
module.exports = (function () {
// ... assume the following methods are revealed. Other private methods/fields are hidden
let priorityQueue = {
insert,
removeMax,
isEmpty,
toString
};
return {
priorityQueue,
newObj: function(comparer, swapper){
let array = [];
let instance = Object.create(priorityQueue);
instance.size = 0;
instance.less = comparer;
instance.swap = swapper;
return instance;
}
}
})();
the reason class syntax was implemented directly into js was just to remove the need to seek that answer. if you really want to go that deep, you should just read the book i mentioned below my answer.
to give you an example of intentional usage of closures to grant private data, i'm going to create a little code example just for this occasion.
keep in mind it's just an example of a concept and it's not feature complete at all. i encourage you just to see it as an example. you still have to manage instances because the garbage collector will not clean them up.
// this will be the "class"
const Thing = (function(){
// everything here will be module scope.
// only Thing itself and it's instances can access data in here.
const instances = [];
// private is a reserved word btw.
const priv = [];
// let's create some prototype stuffz for Thing.
const proto = {};
// this function will access something from the module scope.
// does not matter if it's a function or a lambda.
proto.instanceCount = _=> instances.length;
// you need to use functions if you want proper "this" references to the instance of something.
proto.foo = function foo() {return priv[instances.indexOf(this)].bar};
const Thing = function Thing(arg) {
// totally will cause a memory leak
// unless you clean up the contents through a deconstructor.
// since "priv" and "instances" are not accessible from the outside
// the following is similar to actual private scoping
instances.push(this);
priv.push({
bar: arg
});
};
// let's assign the prototype:
Thing.prototype = proto;
// now let us return the constructor.
return Thing;
})();
// now let us use this thing..
const x = new Thing('bla');
const y = new Thing('nom');
console.log(x.foo());
console.log(x.instanceCount());
console.log(y.foo());
there is a great book called "Pro Javascript Design Patterns" by Dustin Diaz and Ross Harmes. it's open free theese days: https://github.com/Apress/pro-javascript-design-patterns
it will in depth explain certain design patterns that aimed to solve exactly this answer long before we got classes etc. in javascript.
but honestly.. if you want to go further and add something like "extend" or calling functions of the super class.. dude srsly.. just use classes in js.
yes it's all possible in plain vanilla but you don't want to go through all the hassle of creating gluecode.
Is there a way to get the file name from a reference to a class? Please note this example is over simplified to illustrate what I'm trying to do (don't start suggesting logging libraries please!)
//Logger1.js
class Logger1 {
}
//MainProcess.js
class MainProcess {
startChildProcess(Logger) {
//This extension doesn't work, but looking to find something similar that does:
Logger.fileName = () => {
return __filename
}
let loggerFileName = Logger.fileName() //Returns "Main.js" not "Logger1.js", so no good...
childProcess.fork(processFileName, [loggerFileName] )
}
}
//In a child process spawned by main:
loggerPath = process.argv[2]
let Logger = require(loggerPath)[path.basename(loggerPath).replace(".js","")]
let logger = new Logger()
I can obviously add a property with a string value, or a method to return __filename the Logger1 class, but I'd rather avoid it. Is there a way to do this from inside the MainProcess class, keeping Logger1 and any other external code clean?
The reason I don't pass the instance of the logger, is the main process then creates child processes, and these child processes instantiate their own loggers. And there is no way to pass object references down to child processes as far as I'm aware.