Include patterns

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

<!DOCTYPE html>
<html>
  <head>
  <base target="_top">
    <?!= Include.html(['cdn.css']); ?>
    <?!= Include.css(['app']); ?>
  </head>
  <body>
    <?!= Include.html(['cdn', 'appmarkup']); ?>
    <?!= Include.gs(['Packer', 'KeyStore']); ?>

    <?!= Include.mjs({
      sources: [{ 
        key: 'CountryFlag',
        src: 'https://cdn.skypack.dev/vue-country-flag',
        exportName: 'default'
      }, { 
        key: 'Qottle',
        src: 'https://cdn.skypack.dev/qottle',
        exportName: 'default'
      }, { 
        key: 'TimeSimmer',
        src: 'https://cdn.skypack.dev/timesimmer',
        exportName: 'default'
      }], 
      promiseName: 'Store.moduleImports',
      initialize: false
    }) ?>

    <?!= Include.js(['components','store','tabvisibility','provoke']); ?>
    <?!= Include.vue([
      'bmMain',
      'bmChip', 
      'bmThrow',
      'bmChiptool',
      'bmPollstats',
      'bmRateslider',
      'bmPollcontrol',
      'bmTransfer',
      'bmSummary',
      'bmNametip',
      'bmTimestring',
      'bmTimechip',
      'bmInfo'
    ]); ?>
    <?!= Include.js(['main']); ?>
  </body>
</html>
index.html
Let’s look at what each are doing

cdn files

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.

<script src="https://cdn.jsdelivr.net/npm/babel-polyfill/dist/polyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.5.1/vuex.min.js" integrity="sha512-n/iV5SyKXzLRbRczKU75fMgHO0A1DWJSWbK5llLNAqdcoxtUK3NfgfszYpjhvcEqS6nEXwu7gQ5bIkx6z8/lrA==" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.3.20/dist/vuetify.min.js"></script>
cdn.html

<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Material+Icons" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.3.20/dist/vuetify.min.css" rel="stylesheet">
cdn.css.html

markup files

Since this is a Vue.js app, the html markup is very small. The Vue.js components are loaded separately and indivually.
<div id="app">
  <template>
    <v-app>
      <bm-component />
      <bm-throw />
    </v-app>
  </template> 
</div>
appmarkup.html

js files

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
 <?!= Include.js(['components','store','tabvisibility','provoke']); ?>
including js files

gs files

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>

		</v-card-actions>
	</v-card>
</template>
<script>
	import bmChip from '@/components/bmChip'
	export default {
		components: {
			'bm-chip': bmChip
		},
		data: () => {
			return {
				item: {
					avatar: 'https://lh3.google.com/u/0/ogw/ADGmqu972lwKrPZ_57o6Y0fwnTRNNxd-NQB0JYNOe-Q5=s64-c-mo',
					avatarColor: 'indigo',
					name: 'bruce'
				}

			}
		}

	}
</script>
bmInfo.vue

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>

      </v-card-actions>
      
    </v-card>
  `,
  data: () => {
    return {
      item: {
        avatar: 'https://lh3.google.com/u/0/ogw/ADGmqu972lwKrPZ_57o6Y0fwnTRNNxd-NQB0JYNOe-Q5=s64-c-mo',
        avatarColor: 'indigo',
        name: 'bruce'
      }
      
    }
  }
 })
bmInfo.vue.html

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

    <?!= Include.vue([
      'bmMain',
      'bmChip', 
      'bmThrow',
      'bmChiptool',
      'bmPollstats',
      'bmRateslider',
      'bmPollcontrol',
      'bmTransfer',
      'bmSummary',
      'bmNametip',
      'bmTimestring',
      'bmTimechip',
      'bmInfo'
    ]); ?>
index.html

mjs files

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.

    <?!= Include.mjs({
      sources: [{ 
        key: 'CountryFlag',
        src: 'https://cdn.skypack.dev/vue-country-flag',
        exportName: 'default'
      }, { 
        key: 'TimeSimmer',
        src: 'https://cdn.skypack.dev/timesimmer',
        exportName: 'default'
      }, { 
        key: 'Qottle',
        src: 'https://cdn.skypack.dev/qottle',
        exportName: 'default'
      } ], 
      promiseName: 'Store.moduleImports',
      initialize: false
    }) ?>
index.html

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

<script type='module'>
  Store.moduleImports = Promise.all([
    import ('https://cdn.skypack.dev/vue-country-flag'),
    import ('https://cdn.skypack.dev/qottle'),
    import ('https://cdn.skypack.dev/timesimmer')
  ])
  .then(modules=>({
    CountryFlag: modules[0].default,
    Qottle: modules[1].default,
    TimeSimmer: modules[2].default
  }))
</script>
include mjs generated code

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