So, I am doing a small project in Google Apps Script, to make adding/exporting leads from it...less painful.
How do I plan to do this?
I plan on doing this via adding some Actions menu, for the importing and exporting of leads. The imported sheet will, for now, be assumed to be of the same columns as the Google Sheet this script is bound to. (We can support some sheet column conversion features later, but it's probably a YAGNI for my use case.) The exported sheet will be converted from the columns of this sheet, to some simplified, ready-to-send-to-the-mailers columns.
How do I plan to code this (or how am I coding this)?
I am using MVVM design pattern, and have spent last night plugging away at writing MVVM wrappers for everything that I need to (keeping KISS in mind).
The MenuItemViewModels have some name,functionName that the Google Apps Script seem to be looking for. I note that there is some major pain-in-the-ass limitation, though: Google Apps Script wants function NAME and it cannot be method!
OK, show me some code or gtfo
I have some SpreadsheetPageViewModel that look like this:
class SpreadsheetPageViewModel extends BaseViewModel {
init() {
this.exportVM = new ExportSpreadsheetEditViewModel();
this.importVM = new ImportSpreadsheetEditViewModel();
this.menuVM = new MenuViewModel(new MenuModel(),
[
'exportLeads', // this is utility function. I want/need to use openExport method
'importLeads', // this is utility function. I want/need to use openImport method
]);
this.childEditVM = null;
}
openExport() {
this.childEditVM = this.exportVM;
this.childEditVM.view.doShow();
}
openImport() {
this.childEditVM = this.importVM;
this.childEditVM.view.doShow();
}
}
The business logic for the modals that spawn on menu item click, will live in the child view models to this: the ExportSpreadsheetEditViewModel and ImportSpreadsheetEditViewModel.
I was trying to get around the limitation via this hack:
changing
function onOpen(event) {
// show the menu here....
new SpreadsheetPageView().doShow();
}
to something like:
var mainView;
function onOpen(event) {
mainView = new SpreadsheetPageView();
// show the menu here....
mainView.doShow();
}
and then, in the MenuActionUtils.gs, crawling down that mainView like:
function exportLeads() {
mainView.viewModel.showExport();
}
function importLeads() {
mainView.viewModel.showImport();
}
What was the result of that hack?
It didn't work. Why? Because when Google Apps Script fired that exportLeads (or importLeads), mainView was no longer defined!!
Does this mean I have to give up my approach?
How can I use the main view/view model in the onClick of the menu items?
Failing all that, is there a way to create our menu, using this MVVM design pattern (and some HTML/React/....), and inject it in?
By using Google Apps Script it's not possible to modify the look and feel of a Google Workspace editor (Docs, Forms, Sheets, Slides) custom menu, in other words, it's not possible to use HTML/React for this but you might use them in dialogs/sidebars.
Regarding using a design pattern, you might use any design pattern that you want but you should have in mind that every time that a Google Apps Script is triggered by an event the whole project is loaded, so if you need that some objects persist between events then you should find a place to save those objects.
To store an object you might use the Google Apps Script Properties Service and/or the Cache Service, just bear in mind that you should convert it to JSON before saving it. Also you might use a Google spreadsheet but this has several limitations or you might use an external service, i.e. nosql database, by using Google Apps Script URL Fetch service.
Related
using and modifying global variables within handler functions
Styling a custom spreadsheet menu item using Google Apps Script
How to define global variable in Google Apps Script
Google Apps Script (V8); why can't I use an object instance inside of onOpen?
With #Ruben's help, I was able to get this working!
What I did
I didn't give up on the MVVM/OOP design.
Instead, I created singleton static method on the drive class, like so:
static GetMainInstance() {
if (!this._mainInstance) {
this._mainInstance = new this();
}
return this._mainInstance;
}
and use it instead of directly creating the new drive object.
Also, it is view's responsibility to spawn stuff, so I did some refactoring:
In PageView.gs I added the following methods:
showExport() {
this.viewModel.showExport((childVM) => {
this.editView.viewModel = childVM;
this.editView.doShow();
})
}
showImport() {
this.viewModel.showImport((childVM) => {
this.editView.viewModel = childVM;
this.editView.doShow();
})
}
in the PageViewModel.gs, I changed the methods to accept onDone callback:
showExport(onDone) {
this.childEditVM = this.exportVM;
onDone(this.childEditVM);
}
showImport(onDone) {
this.childEditVM = this.importVM;
onDone(this.childEditVM);
}
Simple fix, it works, while staying consistent with the principles!
Related
I want to use SCEditor in my Blazor page.
For example I create a new Blazor WASM project and I did these steps:
According to documentation I add this codes and references to index.html:
https://cdn.jsdelivr.net/npm/sceditor#3/minified/themes/default.min.css
https://cdn.jsdelivr.net/npm/sceditor#3/minified/sceditor.min.js
https://cdn.jsdelivr.net/npm/sceditor#3/minified/formats/bbcode.min.js
window.InitEditor = () => {
var textarea = document.getElementById('myTextArea');
sceditor.create(textarea, {
format: 'bbcode',
style: 'https://cdn.jsdelivr.net/npm/sceditor#3/minified/themes/content/default.min.css'
});
};
Add a textarea in counter page:
<textarea id="myTextArea" style="width:100%; height:200px;"></textarea>
add follow code to code section of counter page:
protected override Task OnAfterRenderAsync(bool firstRender)
{
JsRuntime.InvokeVoidAsync("InitEditor");
return base.OnAfterRenderAsync(firstRender);
}
Now I run the project and I see this editor:
then I click on fetch data menu and I see this:
Surprisingly, editor has shown in fetch data page. if I click on counter page again I see this:
There are 2 Editors O-O. and if I click on this menus then editors are increasing...
I changed the code this way:
protected override Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
JsRuntime.InvokeVoidAsync("InitEditor");
return base.OnAfterRenderAsync(firstRender);
}
the editor is shown once in counter page and fetch data page. Again I don't have any textarea in fetch data page.
How can I solve this problem?
Blazor documentation warns:
Only mutate the Document Object Model (DOM) with JavaScript (JS) when the object doesn't interact with Blazor. Blazor maintains representations of the DOM and interacts directly with DOM objects. If an element rendered by Blazor is modified externally using JS directly or via JS Interop, the DOM may no longer match Blazor's internal representation, which can result in undefined behavior. Undefined behavior may merely interfere with the presentation of elements or their functions but may also introduce security risks to the app or server.
This guidance not only applies to your own JS interop code but also to any JS libraries that the app uses, including anything provided by a third-party framework, such as Bootstrap JS and jQuery.
SCEditor is exactly one of those DOM-mutating libraries, and the effects of failure to observe that guidance you can see for yourself. (The ‘security risks’ bit is rather nonsensical: if your app can be made insecure merely by modifying client-side code, then it wasn’t very secure to begin with. But it’s otherwise good advice.)
Blazor does provide some interoperability with external DOM mutation in the form of element references. The documentation again warns:
Only use an element reference to mutate the contents of an empty element that doesn't interact with Blazor. This scenario is useful when a third-party API supplies content to the element. Because Blazor doesn't interact with the element, there's no possibility of a conflict between Blazor's representation of the element and the Document Object Model (DOM).
Heeding that warning, you should probably write something like below (not tested). In the component file (.razor):
<div #ref="sceditorContainer"></div>
#inject IJSRuntime js
#code {
private ElementReference sceditorContainer;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await js.InvokeVoidAsync("initEditor", sceditorContainer);
}
}
}
And in JavaScript:
function initEditor(container) {
const textarea = document.createElement('textarea');
container.appendChild(textarea);
sceditor.create(textarea, {
format: 'bbcode',
style: 'https://cdn.jsdelivr.net/npm/sceditor#3/minified/themes/content/default.min.css'
});
}
If the SCEditor library is sufficiently well-behaved, it should only modify the DOM tree at most at the level of the parent of the textarea node you give it. You may think it would be enough to place a <textarea> in your component markup and capture a reference to that, but as it happens SCEditor adds siblings to that node, which may keep messing up rendering. For that reason, it is safer to put everything in an initially-empty wrapper element, which can act as a sandbox in which SCEditor has free rein.
Ideally, you would encapsulate everything SCEditor-related into a dedicated component that deals only with SCEditor, while the rest of your app would use that component like any other Blazor component.
I've been attempting to find a way to run scripts across multiple worksheets in a specific order. Each worksheet has its own number of scripts that run just fine when I use them (by creating a "combined" script attached to a button and/or menu).
There are many scripts and going through each one manually to run scripts is not efficient.
I've been trying to utilize libraries, but have not found a way to make it work correctly.
I've seen the Google Apps Script section on libraries, and relevant stackoverflow threads like this one, but haven't been able to figure it out.
Here's the last attempt I put together (after trying many different methods...)
function mastercombine() {
const west = SpreadsheetApp.openByUrl(`https://docs.google.com/spreadsheets/d/1NmN7h2wEGFlY1FatjEpJ9MRpZ_I_MtlyoApGG4F6rcw/edit#gid=589651642`);
west.West.combine();
freezeValues1();
}
I think maybe I'm not entirely understanding how to call each item within the library to run the function on its respective worksheet. I've tried calling it a few different ways, but I'm stumped because they all give null errors.
Textual error I receive:
TypeError: Cannot read property 'masterSheetInfo' of undefined
mastercombine # Copy of Combined Reset.gs:5
The section beginning with west.West is attempting to call library worksheets/functions. "west" being the const, "West" being the Library ID, "combine" being the function within West (which runs correctly on its own within its own worksheet).
The ending section (freezeValues1) is a function that should run on the current worksheet (and it works stand-alone without library section).
The closest I've come to getting things to run in a certain order is attaching installable triggers to each worksheet, but I can't have the "will run sometime within the hour" bit that seems to come with it. I'm looking to call them back-to-back with one trigger.
Script being called from within the library:
function combine() {
sortSheets();
SpreadsheetApp.flush();
Utilities.sleep(500);
sheetNames();
}
which calls the following function:
function sheetNames() {
var ss, list, tar;
ss = SpreadsheetApp.getActive()
list = ss.getSheets().slice(4).map(function (s, ind) {
return ['=HYPERLINK("https://docs.google.com/spreadsheets/d/1tfi_L668w7hpDqeGl84TEfVIm4zKMkyuwrKsG-uKx9A/edit#gid=' + s.getSheetId() + '", "' + s.getName() + '")'];
})
tar = ss.getSheets()[0]; //List will be written to FIRST sheet in the workbook.
tar.getRange(2, 1, tar.getLastRow(), 1).clearContent();
tar.getRange(2, 1, list.length, 1).setValues(list)
}
I'm afraid that is not possible
Not via Apps Script itself anyway. You can run functions remotely, but only via the Apps Script API.
https://developers.google.com/apps-script/api/how-tos/execute
Though that comes with its own complexity and limitations.
Even triggers will not fire from changes to the sheet made by other scripts. So you can't have a utility sheet where your external script can change something and have an onChange trigger on the sheet.
From:
https://developers.google.com/apps-script/guides/triggers/installable#restrictions
Script executions and API requests do not cause triggers to run.
Workaround
Why is it necessary for the scripts to live on the target spreadsheets? Can't you just have a master script that contains all the code and then operates on all the spreadsheets?
For example:
function main() {
let ssIds = [
"1b2RdsafasdfasdfE4GZy5y5465y54y54sMw",
"1b2R40ierqwerqwerf9Mvkm6I-TqE4GZsyMw",
"1b2R40iHFqqwerewqrwerkm6I-TqE4GZysMw",
"1b2R40iHFqOV7-ytqwertywerkm6I-TqGZsMw",
"1b2R40iHFqOVtryertyerf9Mvkm6I-T4GZsMw"
];
ssIds.forEach(id => {
let ss = SpreadsheetApp.openById(id);
// Do stuff here.
//...
//...
})
}
From the question
The section beginning with west.West is attempting to call library worksheets/functions. "west" being the const, "West" being the Library ID, "combine" being the function within West (which runs correctly on its own within its own worksheet).
A Google Apps Script library hasn't worksheets.
west.West is not working because west has assigned a Class Spreadsheet object and this class hasn't a West property/method. Because of this west.West returns undefined.
One option is to convert West.masterSheetInfo() into a parametized function i.e. West.masterSheetInfo(west)
NOTE: If the parametized function, has a line like
const west = SpreadsheetApp.getActiveSpreadsheet();
might be replaced by something like
var west = west || SpreadsheetApp.getActiveSpreadsheet();
Regarding the order of execution, in Google Apps Scripts function statements are executed in the order they are wrote.
Resources
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions
Related
Order of execution of functions in functional programming
Google script - call library function with .getActiveSpreadsheet()
I'm currently working on the front-end of a medium/large-scale data-driven Asp.net MVC application and I have some doubts about the right code-organization/design pattern to follow.
The web application is made by multiple pages containing many Kendo UI MVC widgets defined with Razor template.
For those who are unfamiliar with Kendo, the razor syntax is translated to Javascript as the following snippet:
I defined inside my Script folder two main folders, and I structured my js files as follow:
shared //Contains the shared js files
-file1.js
-file2.js
pages //One file per page
page1.js
page2.js
...
Ticket.js // page 4 :)
Each js file is a separate module defined with the following pattern:
Note: Inside init function is registered every callback function to the window events and occasionally a $(document).ready(function(){}) block.
;(function () {
"use strict";
function Ticket(settings) {
this.currentPageUrls = settings.currentPageUrls;
this.currentPageMessages = settings.currentPageMessages;
this.currentPageEnums = settings.currentPageEnums;
this.currentPageParameters = settings.currentPageParameters;
this.gridManager = new window.gridManager(); //usage of shared modules
this.init();
}
Ticket.prototype.init = function () {
$("form").on("submit", function () {
$(".window-content-sandbox").addClass("k-loading");
});
...
}
Ticket.prototype.onRequestStart = function (e) {
...
}
//private functions definition
function private(a, b, c){
}
window.Ticket = Ticket;
}());
Once I need my Javascript functions defined in a module I include the associated Javascript file in the page.
An istance of my object is stored inside a variable and, on top of that, a function is bound to the widget event (see: onRequestStart).
HTML/JAVASCRIPT
#(Html.Kendo().DropDownList()
.Name("Users")
.DataValueField("Id")
.DataTextField("Username")
.DataSource(d => d.Read(r => r.Action("UsersAsJson", "User"))
.Events(e => e.RequestStart("onRequestStart"))))
var settings = {};
var ticket = new window.Ticket(settings);
function onRequestStart(e){
ticket.onRequestStart(e);
}
I feel like my design pattern might be unfriendly to other front-end delevoper as I am, mostly because I choose not to implement the Javascript modules within Jquery plugin.
First, Am I doing everything the wrong way?
Second, is my design pattern suitable for a Javascript test-framework?
Third, which are the must-have scenarios for Jquery plugins?
Update
Added the Javascript output by the above Razor syntax.
Folder structure
In terms of functionality (shared) and modules (modular approach), the development or application code should represent what you can encounter in HTML. A simple ctrl+f over your solution should yield all possible changes. From that experience over the years I personally prefer dividing it in:
app (application code)
classes (reusable)
modules (singleton)
lib (package manager/grunt/gulp/...)
jquery (proper library names/unminified dist file or root file)
kendo
File names
Representing what something does and to be able to reuse it in a blink of an eye is what will cut your development time. Choosing proper names has value as I'm sure you are aware. My file names always starts with the namespace usually in short followed by a reusable "search" term:
app/prototypes
ns.calendar.js (multiple configs)
ns.maps.js (combinations or single uses)
ns.places.js (forms or map add-ons)
ns.validation.js (multiple forms and general handling)
app/singletons
ns.cookiebox.js (single config)
ns.socialmedia.js (single config)
ns.dom.js (provides a place for dom corrections, global resize events, small widgets, ...)
To add, what you called shared, is functionality that's meant to be global. A great example would be to use underscore library. Or create a collection of functions (device detection, throttle, helpers in general) on your own to reuse throughout projects => ns.fn.js
Since you add them only once throughout your namespace, it's also built as singleton and can be added to the modules folder or directly in the app root.
As last addition a loader file to kickstart your point of control => ns.load.js in the app root. This file holds the single DOM ready event to bind protoypes and modules.
So you might want to rethink your idea of dividing into pages. Trust me, I've been there. At some point you'll notice how functionality grows too large in order to configure all pages separately and therefor repeatedly.
File structure
To be honest I like Tip 1 of #TxRegex answer the most, with a small addition to bind the namespace and pass it from file to file as it get's loaded.
Core principle: IIFE bound to window object
window.NameSpace = (function($, ns){
'strict'
function private(){}
var x;
ns.SearchTerm = {};
return ns;
}(window.jQuery, window.NameSpace || {}));
For more example code I'd like to point out my github account.
Bundling
Try to achieve a single bundled and minified file from lib to app, loaded in the head on async for production releases. Use separated and unminified script files on defer for development and debug purposes. You must avoid inline script with global dependencies throughout the whole project if you do this.
path to js/lib/**/*.js (usually separated to keep sequential order)
path to js/app/ns.load.js
path to js/app/ns.fn.js
path to js/app/**/*.js (auto update the bundle)
Output => ns.bundle.js
=> ns.bundle.min.js
This way you'll avoid render blocking issues in JavaScript and speed up the loading process which in turn boosts SEO. Also enables you to combine functionality for mobile layouts and desktop layouts on the fly without memory issues or jerky behavior. Minifies really well and generates little overhead in calling instances from the loader file. As a single bundle will be cached throughout your pages it all depends on how many dependencies or libraries you can cut from the bundle. Ideally for medium and large projects where code can be shared and plugged in to different projects.
More info on this in another post.
Conclusion
First, Am I doing everything the wrong way?
Not at all, your modular approach seems ok...
It's missing a global namespace, which is hard to avoid without at least one. You create one for each module but it seems better to group them all under one namespace so you can differentiate library code from application code in the window object.
Kendo seems to create inline scripts? Can't you counter the placement server side?
Second, is my design pattern suitable for a Javascript test-framework?
Except for the Kendo instances, you can add a layer for testing purposes. Remember if jQuery is your dependency inline, you'll have to render block it's loading. Otherwise => jQuery is undefined
Exclude Kendo dependencies from the bundle if you can't control the inline script. Move to a </body> bundled solution.
Third, which are the must-have scenarios for Jquery plugins?
modular approach
configurable approach for multiple instances (tip: moving all strings from your logic, see how Kendo uses object literals)
package manager to separate the "junk" from the "gold"
grunt/gulp/... setup to separate scss and css from js
try to achieve a data-attribute binding, so once all is written, you configure new instances through HTML.
Write once, adapt easily where necessary and configure plenty!
The organization and pattern seems fine, but I have some tips:
Tip 1:
Instead of setting specific global variables within your module, perhaps you could return the object instead. So instead of doing this:
;(function () {
"use strict";
function Ticket(settings) {
console.log("ticket created", settings);
}
...
window.Ticket = Ticket;
}());
You would do this:
;window.Ticket = (function () {
"use strict";
function Ticket(settings) {
console.log("ticket created", settings);
}
...
return Ticket;
}());
The reason for this is to be able to take your module code and give it a different global variable name if needed. If there is a name conflict, you can rename it to MyTicket or whatever without actually changing the module's internal code.
Tip 2:
Forget Tip 1, global variables stink. Instead of creating a seperate global variable for each object type, why not create an object manager and use a single global variable to manage all your objects:
window.myCompany = (function () {
function ObjectManager(modules) {
this.modules = modules || {};
}
ObjectManager.prototype.getInstance = function(type, settings) {
if (!type || !this.modules.hasOwnProperty(type)) {
throw "Unrecognized object type:";
}
return new this.modules[type](settings);
};
ObjectManager.prototype.addObjectType = function(type, object) {
if (!type) {
throw "Type is required";
}
if(!object) {
throw "Object is required";
}
this.modules[type] = object;
};
return new ObjectManager();
}());
Now each of your modules can be managed with this single global object that has your company name attached to it.
;(function () {
"use strict";
function Ticket(settings) {
console.log("ticket created", settings);
}
...
window.myCompany.addObjectType("Ticket", Ticket);
}());
Now you can easily get an instance for every single object type like this:
var settings = {test: true};
var ticket = window.myCompany.getInstance("Ticket", settings);
And you only have one global variable to worry about.
You can try separating your files in different components asuming each component has a folder.
for example: page 1 is about rectangles so you make a folder call rectangle inside that folder you create 3 files rectangle.component.html, rectangle.component.css, rectangle.component.js (optional rectangle.spec.js for testing).
app
└───rectangle
rectangle.component.css
rectangle.component.html
rectangle.component.js
so if anything bad happends to a rectangle you know where is the problem
a good way to isolate variables and execute in the right place is to use a router basically what this does it check at the url and executes the portion of code you asign to that page
hope it helps let me know if you need more help.
I am currently working on large projects that make use of lots of javascript files.
I then start learning of using local scopes and using objects.
What I do not really understand is how to call them into you local scope?
E.g if I create an object in an local scope in file-a, how can I use them as in a function in the document.ready scope file-b?
I get that you can find this online, but I get demotivation by the high amount of javascript on the internet and can't really find good examples or material. Any help?
Not sure,but I think you might be referring to the use of namespaces within JavaScript as a way to avoid adding all your functions to the main window object.
The Ugly Way
Let's assume you have 3 functions related to cats:
Function AddCat(cat) {
}
Function DeleteCat(catId) {
}
Function BreedCat(cat,cat) {
}
The way these items are coded, they are globally available. Not only does that clutter up your window object, it's hard to share data between these functions in a discreet way.
As long as this js file is loaded, any function in your app can call these functions just by calling AddCat()
Cleaner
To solve that problem, we could create a Cats object that acts as a "namespace" here:
Cats = {
AddCat: function(cat) {
},
DeleteCat: function(catId) {
},
BreedCat: function(cat,cat) {
}
}
Now, you've only added ONE object to the windows class: Cats. In addition, other methods in your web app can call any of those 3 items by calling Cats.AddCat() for example.
This lets you encapsulate all of the Cat data in your entire system within a single "namespace, so it's easier to read.
This can get a lot more detailed. By encapsulating items like this, you can start to hide variables that all your cat routines require from the rest of your code.
There is an excellent set of resources on this type of namespacing (including tons of detail) here and here with links that lead you deeper.
Is that what you were looking for?
I use cordova plugin with Application Insight named cordova-plugin-ms-appinsights (https://github.com/MSOpenTech/cordova-plugin-ms-appinsights/blob/master/README.md) and I tried to add my properties to context, that through each request application will send additional info, for example code name of my application.
I tried as below:
appInsights.context.application.codename = "code name app";
appInsights.trackEvent("my event");
and this did not work.
Can I add additional info to context?
There are 2 issues:
1) Cordova plugin seems to use an old version of the JS SDK. You can try to update it manually after it pulls down the old one (take the most recent one from https://github.com/Microsoft/ApplicationInsights-JS/blob/master/dist/ai.0.js)
2) The feature that adds data to ALL telemetry items is not released yet. It was implemented recently - see JS SDK commit on github. You can either wait a bit until it's released or get the latest from master and compile it yourself (and take the result from /JavaScript/min/ai.min.js)
A hacky alternative may be to create a wrapper on top of SDK methods like trackEvent() which adds the data you need (I'm sorry for giving you the JS SDK code equivalent as I haven't used cordova plugin myself):
// this is you implementation of custom data to be attached
// to all telemetry items.
// It needs to return JSON - as it's expected format for custom properties.
// In this specific case you'll create a custom property 'hey' with value 'yo'
var getMyDataJson = function() {
return { hey: "yo"};
}
// this is your wrapper, you'll call it instead of appInsights.trackEvent()
var myTrackEvent = function(data) {
var toBeAttachedToAllItems = getMyDataJson();
appInsights.trackEvent(data, toBeAttachedToAllItems);
}
<...>
// somewhere later you track your telemetry
// this will call your getMyDataJson() function which'll attach
// custom data to your event.
myTrackEvent("tracked!")