When using HtmlService it can be quite messy to organize your script/js/html/css files and vue components so that they can be inserted into your add-on/webapp. This becomes much more complex when you want to include frameworks like Vue.js which are generally written in a Node environment and later bundled using tools like webpack. Furthermore, now that modules are supported in Es2020, thousands of useful modules from npm, many of which don’t have ‘bundled’ entry points, are now accessible in Add-ons.
This Include pattern is available on Github standalone and also as part of the Vuejs template on which this article is based.
Using a bundler
If you are already using Clasp, or some other bundling solution to develop Apps Script in using local Node tooling, then you will have already solved many of these issues, but many developers prefer to stay completely inside the Apps Script IDE, especially since we now have an updated environment. In this article we’ll look at patterns that enable you to do all these things, but without leaving the Apps Script environment.
File types
In Apps Script, there are only 2 types of file.
.gs files – these are intended to be run server side
.html files – these are intended to be used as input to htmlservice
In this pattern we’ll expand on these file types a bit
.gs files – these are server side files, but can also be included client side to allow common code to run both server side and client side. Of course .gs files that contain references to Apps Script services can only run server side, but everything else is fair game.
.html files – these should contain markup html only
.js.html – client side javascript.
.css.html – css style files
.vue.html – vue scripts
.mjs.html – javascript ES modules
Tags
There’s no need to include <script>, <style> tags etc, as they will be inserted automatically if needed.
Index file
The index file is used to organize the finally bundled htmlservice app. Here’s an example of a full app that include various of the above examples. The most complex is the .mjs file, which is for importing modules – often directly from the npm cdn
Many apps will include script files and css from a cdn – best to keep those separate.
In this example, which uses the Vue.js framework we’re going to use cdn hosted vue.js, vuex and vuetify. You’ll see when we get to modules that we could have included them in that way, but let’s just include the bundled versions for this. We also include a babel polyfill to ensure we can support es6 and es2020. Here’s the css and javascript we’ll need.
These are regular JavaScript files that need to be included in the client app. Like all client side apps, any variables defined here will be in the global space. I recommend no more than one variable definition per file, and that variable should define a namespace or a single class. I tend to use a Store to hold all variables thata would otherwise be global to minimize conflicts. You can if course include a list of js files just like this
The same recommendations apply to gs files as those that apply to js files – namely to keep global space as uncluttered as possible. Normally these are going to be run Server side, but it’s possible that there is code you want to be able to run on both sides. Another upside of this approach is that the new IDE will apply JavaScript autocomplete, formatting and validation as it does to Server side code.
<?!= Include.gs(['Packer', 'KeyStore']); ?>
Packer.gs
Vue.js files
Normally Vue will be developed on Node with some tooling to build and bundle the app. Of course you can’t do this from the Apps Script IDE, so we need some special treatment.
A normal vue file will look like this, so the challenge is how to pull a bunch of components into an htmlservice app
<template> <v-card> <v-card-title> About this Vue.js template </v-card-title> <v-card-text> You can use this template to build Apps Script add-ons and web apps that use Vue.js, Vuex and Vuetify and spreadsheet polling </v-card-text>
<v-card-actions>
<bm-chip :item="item" /> <v-spacer></v-spacer> <v-btn color = "accent" target="_blank" href="https://ramblings.mcpher.com/vuejs-apps-script-add-ons/"> info </v-btn>
The entire app has a namespace named Store – into which the majority of app resources are. The idea is that the code for Vue components are each added to this store – so the here’s how a vue file for htmlservice would like and how it would be added to the store
Store.addComponent ({ name: 'bm-info', template: ` <v-card> <v-card-title> About this Vue.js template </v-card-title> <v-card-text> You can use this template to build Apps Script add-ons and web apps that use Vue.js, Vuex and Vuetify and spreadsheet polling </v-card-text>
<v-card-actions>
<bm-chip :item="item" /> <v-spacer></v-spacer> <v-btn color = "accent" target="_blank" href="https://ramblings.mcpher.com/vuejs-apps-script-add-ons/"> info </v-btn>
Each component is listed in the index.html, and each component file would be defined as the above example. Here’s a list of components used in this app
es2020 introduced dynamic import to JavaScript. This provides an alternative to a bundled cdn, but also introduces the possibility of including a myriad of unbundled Node scripts from npm that would otherwise not be usable in htmlService. It also means that vue components can be imported, just as if you’d imported them through node.js tooling.
First a couple of points on importing.
<script type=”module”> introduces the capability of importing modules.
Most npm modules are automatically available at https://cdn.skypack.dev/[npmmodule]
Although some of these modules include bundled versions, not all do, so you have to use module importing to get access to them
Imports are scoped to within the <script type=”module”></script> tags, whereas normally <script></script> will include its contents in the global space.
Imports are asynchronous
These things together mean that we need some measures to be able to mix modules, and traditional scripts. Include.mjs takes care of all of this. Heres an example.
The idea is that a single object will hold all the imported modules in the form of a promise, which will be resolved when all the requested modules are finished importing. Later in the app, the Promise can be tested for resolution and the modules available for use.
mjs Property descriptions
An mjs include definition has these properties
property
notes
sources
The list of sources from where to import the modules
promiseName
The name of the variable (or object property) that will hold the promise that will be resolved when all the modules have been imported
initialize
Whether or not the promiseName needs to be created as a global variable
sources property descriptions
Each source element has these properties
property
notes
key
This is how the module will be known later
src
This is from where the module should be imported
exportName
Depending on whether the module is a named export, but most npm modules will be ‘default’
It’s worth taking a quick look at exactly what code will get generated from our Include definition
At some later point, when the imported modules are needed, we can wait till the promise resolution before continuing. This is the complete main.js for the app.
window.onload = () => {
// need to wait for any modules to be imported const waitForModules = Store.moduleImports || Promise.resolve(null)
waitForModules.then(modules=> {
// this is an npm module but we can install just like the others Store.addComponent(modules.CountryFlag)
// initialize everything and register all the components Store.init({modules}) .components .registerAll()
// render vue new Vue({ el: '#app', vuetify: new Vuetify(), store: Store.vxStore })
// start polling Store.load()
/** * we need to detect when the add-on is closed to clean up */ window.addEventListener("beforeunload", (event)=> { Store.unload() // Cancel the event as stated by the standard. event.preventDefault(); // Older browsers supported custom message event.returnValue = ''; });
/** * there's some action that could be taken when the tab becomes visible */ Store.handleVisibility(new TabVisibility()) })
main.js
Importing external vue components
We used Include.vue to add components which were defined local to this project, but ideally we’d like to be able to have access to the many vue components available on npm. These are typically not bundled as they are expected to be used in a Node development environment. However we can use exactly the same module import method to include them in your apps script Vue app.
Note above that a country flag module has been imported – now we can simply add that to our local components
Store.addComponent(modules.CountryFlag)
adding an imported vue module
Summary
Of course the order of including is still important, but following a few basic guidelines can avoid lots of wasted time. The key points here are that we can
Access all modules on npm and include them in our htmlservice app
Reuse code between client and server
Import ready made Vue components
Share with your network
bruce mcpherson is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. Based on a work at http://www.mcpher.com. Permissions beyond the scope of this license may be available at code use guidelines