If you are doing anything with HTMLService you have to duplicate a fair amount of stuff, usually by copying them from previous projects. Not only html and css, but also useful code that you’ve built up over time. Quite recently, Clasp was introduced to Apps Script to allow you to better use github and workflow tools to make all this a little less laborious. However, if you want to continue to use the vanilla IDE there’s a way to do that too.
In all most of my projects that use HTMLService, I use a certain number of code concepts repeatedly – specifically
- standard css for spinners and layouts
- material design (muicss)
- various DOM related utilities
- promisified version of google.script.run that works with namespaced methods
- sharing the same JS code between server and client
- Property services
- A JSON editor (I’m using https://github.com/josdejong/jsoneditor)
- Standardized error reporting and notification
- A modular “Include” method
- Passing arguments from server to client on htmlservice startup
All in all this amounts to a fair amount of setup before you can even get started. However it is possible to build a library of stock code that you can simply include in all your projects. Here’s how.
The example
The UI looks like this
This complete app is not public – it uses a library to do the server side part of the work and I won’t be discussing that here. However the container bound part is generic and I’ll use it to illustrate how you can use a stock html library to simplify and standardize your apps script work – everything fits together like this

Here’s the key, and it’s also on github
1hOH5hCIZSk_PIUKNz_b87HaqsRiH2Im1GeflAf5P814RML1kQjNzkyJ_
How does it work?
Both the cStockClient library and the orchestration app contain the Include namespace, which looks like this, so when building your orchestration app, first create a file called Include containing this, and include a library reference to the cStockClient library (or your version of it).
function getLibraryNamespace (library) { return this[library]; } /** * used to expose memebers of a namespace * @param {string} namespace name * @param {method} method name */ function exposeRun(namespace, method, argArray) { var func = namespace ? this[namespace][method] : this[method]; if (argArray && argArray.length) { return func.apply(this, argArray); } else { return func(); } } /** *used to include code in htmloutput *@nameSpace Include */ var Include = (function (ns) { /** * given an array of .gs file names, it will get the source and return them concatenated for insertion into htmlservice * like this you can share the same code between client and server side, and use the Apps Script IDE to manage your js code * @param {string[]} scripts the names of all the scripts needed * @param {string} [library=""] from a shared library (which has include installed) * @return {string} the code inside script tags */ ns.gs = function (scripts,library) { return library ? getLibraryNamespace(library).Include.gs (scripts) : '<script>\n' + scripts.map (function (d) { // getResource returns a blob return ScriptApp.getResource(d).getDataAsString(); }) .join('\n\n') + '</script>\n'; }; /** * given an array of .html file names, it will get the source and return them concatenated for insertion into htmlservice * @param {string[]} scripts the names of all the scripts needed * @param {string} [ext] an extension like js if required * @return {string} the code inside script tags */ ns.htmlGen = function (scripts, ext, library) { return library ? getLibraryNamespace(library).Include.html (scripts) : scripts.map (function (d) { return HtmlService.createHtmlOutputFromFile(d+(ext||'')).getContent(); }) .join('\n\n'); }; /** * given an array of .html file names, it will get the source and return them concatenated for insertion into htmlservice * inserts html style * @param {string[]} scripts the names of all the scripts needed * @return {string} the code inside script tags */ ns.html = function (scripts,library) { return ns.htmlGen (scripts , '' , library); }; /** * given an array of .html file names, it will get the source and return them concatenated for insertion into htmlservice * inserts css style * @param {string[]} scripts the names of all the scripts needed * @return {string} the code inside script tags */ ns.js = function (scripts,library) { return library ? getLibraryNamespace(library).Include.js (scripts) : '<script>\n' + ns.htmlGen(scripts,'.js') + '</script>\n'; }; /** * given an array of .html file names, it will get the source and return them concatenated for insertion into htmlservice * like this you can share the same code between client and server side, and use the Apps Script IDE to manage your js code * @param {string[]} scripts the names of all the scripts needed * @return {string} the code inside script tags */ ns.css = function (scripts,library) { return library ? getLibraryNamespace(library).Include.css (scripts) : '<style>\n' + ns.htmlGen(scripts,'.css') + '</style>\n'; }; /** * append args to be poked into the resolved template * @param {object} args * @return {string} */ ns.staticArgs = function (args) { if (!args.namespace) throw 'Specify a namespace to contain static args'; return '<script>var ' + args.namespace + '= (function (ns) { ns.params =' + (args.params ? JSON.stringify(args.params): "") + ';return ns;})({});</script>'; }; return ns; })(Include || {});
The final orchestrating app looks like this, and we’ll go into each file in this article. You can ignore the Code.js file. I keep that around for unit testing.
index.html
This defines the structure of the Client side App, and uses the Include namespace to pick up code from both the stock library and itself. Here’s what mine looks like. The idea is to keep this as clean as possible, picking up the code, html and css from either the local project or from the library.
<!DOCTYPE html> <html> <head> <base target="_top"> <?!= Include.html(['cdn.css'],'cStockClient'); ?> <?!= Include.css(['spinner','app'],'cStockClient'); ?> </head> <body> <?!= Include.html(['cdn','jseditcdn','jseditcdn.css'],'cStockClient'); ?> <?!= Include.html(['appmarkup']); ?> <?!= Include.html(['spinner','app'],'cStockClient'); ?> <?!= Include.js(['main']); ?> <?!= Include.gs(['JSEdit','App','DomUtils','Provoke'],'cStockClient'); ?> <?!= Include.gs([ 'Client','Home']); ?> </body> </html>
Include.html (arrayOfFilenames [,libraryName])
This includes code from each of the html type file names mentioned. If a library name is given it will look there (it uses the copy of the Include namespace in the library to do that).
Include.css (arrayOfFilenames [,libraryName])
Gets any css definitions included in the html type file names given. There’s not need for them to container <style> tags as this gets inserted automatically.
Include.gs (arrayOfFilenames [,libraryName])
The idea here is that regular script files can be used client side (and server side too if appropriate). This allows sharing of the same code between client and server.
Include.js (arrayOfFilenames [,libraryName])
Whereas almost all of my client code is written as regular .gs files, if you want to use syntax unsupported by the Apps Script editor, you’ll need to write them as html type files. This pulls those in. There’s no need for script tags, as they’ll be added automatically.
Addon.gs
This script file contains the usual stuff for kicking off HtmlService, but also has a pattern for getting variable data into the htmlservice build using Include.staticArgs().
function doCollection() { // get the sheet var name = SpreadsheetApp.getActiveSheet().getName(); if (!VimeoCommon.Intro['doCollection' + name]) throw 'Dont know how to get collections on this sheet'; return showDialog (); } /** * Opens a dialog. */ function showDialog() { var sheetName = SpreadsheetApp.getActiveSheet().getName(); var static = Include.staticArgs ( { namespace:'AppsScriptStatic', params: { sheetName:sheetName, execute: { namespace:'Server', method:'exec', args:[sheetName] } } }); var ui = HtmlService.createTemplateFromFile('index.html') .evaluate() .append(static) .setSandboxMode(HtmlService.SandboxMode.IFRAME) .setWidth(400) .setHeight(580) .addMetaTag('viewport', 'width=device-width, initial-scale=1'); SpreadsheetApp.getUi().showModelessDialog(ui, 'Collection parameters for '+sheetName); }
Notice that the variable static is filled with some stuff you’d like to build into the htmlservice script. Include.staticArgs() generates some code to create a client side namespace (in this case called ‘AppsScriptStatic’), with various properties that’ll be of some use later. In this case, I’m passing the sheet name, along with the name of a server side namespace and method the client will want to execute later. You can of course put any (stringifable) thing you want here. Just make sure the namespace is specified.
main.js.html
This is main code to be executed when the client side dom is constructed. This should be as clean as possible, and because it contains reference to window (which doesn’t exist server side), it’s written it as a js.html file , rather than a .gs script.
.
window.onload = function() { // set up client app structure App.initialize(); Home.init(); // get the ui going Client.init(AppsScriptStatic); };
Notice that it references the AppsScriptStatic namespace – which doesn’t exist in any code you’ve written. It got created during the Include.staticArgs() phase. This is how variables from the server ended up getting embedded in the client side script.
appmarkup.html
This is the html for the app. Most of the markup, (and all of the css comes from the stock library), so this can be pretty minimal too.
<div id="content-wrapper"> <legend>GraphQL collection parameters</legend> <div class="mui-textfield mui-textfield--float-label"> <input id="textmax" type="text" value=""> <label>Maximum rows to return</label> </div> <div> <label>GraphQL variables</label> <div id="jsoneditor" style="width: 100%; height: 380px;"></div> </div> <div class="button-place"> <button class="mui-btn mui-btn--primary" id="start-button">Execute</button> <button class="mui-btn" id="close-button">Cancel</button> </div> </div>
Server.gs
Would typically contain the server side code. In my case, it’s minimal because I’m using another library to do the server side work. The property store stuff is defined in the stock library so all that’s required are some hooks, and to set up this script’s property service.
var Server = (function (ns) { ns.initProp = function () { // set up propertystore return cStockClient.PropertyStores.init (PropertiesService); }; ns.getProp = function (store , key) { return ns.initProp().get(store, key); }; ns.setProp = function (store , key, ob) { return ns.initProp().set(store, key,ob); }; ns.exec = function (name, params) { return VimeoCommon.Intro['doCollection' + name](params); }; return ns; }) ({});
Home.gs
I generally use this namespace to handle Dom events and validation. This one is rather lengthy as it also contains the JSONschema definition describing the valid input for the JSON editor dialog. An execute button push starts off the Client dialog, which will in turn initiate the Server side code.
/** * sets up all listeners * @constructor Home */ var Home = (function (ns) { 'use strict'; var el; ns.close = function () { return google.script.host.close(); }; // The initialize function must be run to activate elements ns.init = function (reason) { el = DomUtils.elem; el("close-button") .addEventListener ('click', function (e) { ns.close(); }); // start generating el("start-button") .addEventListener ('click', function (e) { var buttons = ["start-button","close-button"]; App.spinCursor(); DomUtils.disable (buttons); Client.start () ['finally'] (function () { App.resetCursor(); DomUtils.enable (buttons); }); }); el("textmax") .addEventListener ('change', function (e) { App.hideNotification(); ns.parseTextMax(); }); // these are the standard parameters allowed for a collection (minus distinct and countRows which are inappropriate for this use case) var schema = { "title": "Collection Schema", "type": "object", "properties": { "limit": { "type": "integer" }, "offset": { "type": "integer" }, "valueKey": { "type": "string" }, "value": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type":"array" } ] }, "op": { "type": "string" }, "sortFields": { "type": "string", }, "sortOrder": { "type": "string", "enum": ["ASC", "DESC"] }, "filters": { "type": "array", "properties": { "valueKey": { "type": "string" }, "value": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type":"array" } ] } } } } }; JSEdit.init(('jsoneditor'), { schema:schema, mode:"text" }); }; // Load text from properties into form ns.adaptTextArea = function (vars) { el("textmax").value = vars && vars.max ? vars.max : ""; // this fixes the floating label if (el("textmax").value) el("textmax").dispatchEvent(new Event('change')); JSEdit.set ( (vars && vars.params) || {}); }; // get the params from the form and validate them ns.getTextArea = function () { return ns.parseTextArea(); }; // check max is a number ns.parseTextMax = function () { var max = el("textmax").value ? parseInt ( el("textmax").value, 10) : ""; if (isNaN (max) ) { App.showNotification ("Invalid number", "max needs to be a number"); return null; } return max; }; // check the contents of jsoneditor is good ns.parseTextVars = function () { try { var params = JSEdit.get(); return params; } catch (err) { App.showNotification ("Invalid JSON", "Graphql vars needs to be valid JSON"); return null; } }; // get the max ans json ns.parseTextArea = function () { var params = ns.parseTextVars(); if (!params) return null; var max = ns.parseTextMax(); if (max === null ) return null; return { params: params, max: max }; }; return ns; })(Home || {});
Client.gs
Finally, the main client code.
var Client = (function (ns) { /** * make sure that we received static parameters */ ns.checkParams = function () { const params = ns.static && ns.static.params; const exec = params && params.execute; if (!params || !params.sheetName || !exec || !exec.namespace || !exec.method || !exec.args) { App.showNotification ('Static params invalid', JSON.stringify(ns.static || "")); return false; } return true; } /** * store the static parans for later */ ns.init = function (static) { ns.static = static; if (ns.checkParams()) { // set the jsoneditor root name // load the default values from last time ns.getVars (ns.static.params.sheetName); return ns; } return null; }; /** * start up the client */ ns.start = function () { if (ns.checkParams()) { //store the latest params var params = Home.getTextArea (); if (params) { // set params to property - wait.. var sv = ns.setVars(ns.static.params.sheetName, params); // get ready to run var exec = ns.static.params.execute; var runArgs = [exec.namespace,exec.method]; Array.prototype.push.apply (runArgs , exec.args); Array.prototype.push.apply (runArgs , [params]); // make sure props are save and go return Promise.all ( [sv,Provoke.run.apply (this,runArgs)]) .then (function (results) { Home.close(); }) ['catch'](function (err) { App.showNotification ("Execution failed" , err); console.log ('failed provoke', err, JSON.stringify(runArgs)); }); } } else { return Promise.reject ("missing args"); } }; /** * make a standard property key */ ns.makeKey = function (key) { return 'fidvars_' + key; }; /** * get the vars fron the property store */ ns.getVars = function (key) { App.spinCursor(); return Provoke.run ("Server", "getProp" , "script" , ns.makeKey(key)) .then (function (vars) { Home.adaptTextArea (vars); App.resetCursor(); }) ['catch'](function (err) { App.showNotification ("Error getting property", err); console.log ('error getting var', key); }) }; /** * store the vars in the property store */ ns.setVars = function (key, ob) { return Provoke.run ("Server", "setProp" , "script", ns.makeKey(key), ob) .then (function (vars) { return vars; }) ['catch'](function (err) { App.showNotification ("Error getting property", err); console.log ('error setting var', key); }); }; return ns; }) ({});
The main points of interest here are the use of the Provoke namespace (from the stock library) to run a promisified version of google.script.run and the use of the static variables passed over when the htmlservice was initially created.
Summary
Most of the laborious stuff in setting up a client side app using HtmlService can be done once off and re-used. If you find something useful, stick it in your stock library and use Include to use it over and over again.
For more like this see Google Apps Scripts Snippets
- A functional approach to fiddling with sheet data
- A functional approach to updating master sheet
- A recursive extend function for Apps Script
- A webapp to share copies of the contents of folders
- Abstracting services with closures
- Add-on spinner
- Addressing namespace and library methods from google.script.run
- Anonymous user registration with the Apps Script PropertiesService
- Apps for Office – binding example comparison
- Apps Script as a proxy
- Apps Script const scoping problems
- Calculating image dimensions in server side apps script
- Calculating the last day of a given weekday in the month
- Canvasser
- Chaining JavaScript
- Changing class properties dynamically
- Checking the argument types in Apps Script
- Cleaning up a document format
- Cleaning up heading levels
- Column numbers to characters
- Composing functions and functional programming
- Configurable canvas meter
- Convert JSON to XML
- Converting SVG to PNG with JavaScript
- Converting timestamps to dates formula
- Copying canvas and svg images from your Add-on
- Copying to new host location
- Copying to new host location
- Counting script and library usage
- Create sha1 signatures with apps script
- Creating a key digest to use for a cache key or to compare content
- Creating a pile of files list from Google Drive
- Creating and working with transposed sheet data arrays
- Cross Origin Resource sharing (CORS)
- CryptoJS libraries for Google Apps Script
- Custom checking for exponential backoff
- Data wrangling with named columns in Google Spreadsheet
- Dealing with objects that are too large for the property or cache store
- Detecting Spreadsheet tables automatically with Google Apps Script
- Direction minimizer – other usages
- Do something useful with GAS in 5 minutes
- Dynamically creating tables with clusterize.js
- EasyCron Library
- ES6 JavaScript features
- Exponential backoff
- Exponential backoff for promises
- Fiddler and rangeLists
- Fiddling with text fields that look like dates
- Filling ranges in Google Sheets
- Finding a Drive App folder by path
- Finding where Drive hosting is being used in Sites
- Flattening an object with dot syntax
- Flattening and unflattening objects to spreadsheets
- Formatting GraphQL queries
- Formatting sheet column data with fiddler
- From notes to frequencies and back again
- From Xml to JSON
- Generating and managing random lists with JavaScript and Apps Script
- Generating coupon codes with expiry dates
- Generating test data for sheets and tables
- Get GAS library info
- Getting an htmlservice template from a library
- Getting insights into Sheets performance
- Google Drive as cache
- Header formatting with fiddler
- Highlight duplicate rows in a sheet – map and reduce
- Highlight duplicate rows in a sheet – map, filter and every
- How to determine what kind of object something is in Apps Script
- How to get stats about youtube videos in your channel with apps script
- How to pass non stringifyable objects to html service
- How to transpose spreadsheet data with apps script
- Identify duplicates on Drive
- Identifying hosted files
- Implementing a client side progress bar reporting on server side work for htmlService
- Importing Predictwise data
- Improved namespace pattern for Apps Script
- Including the stack in custom errors
- JavaScript closures – how, where and why
- JavaScript currying and functional programming
- JavaScript currying and functional programming – even more
- JavaScript recursion primer
- JSONP and JSON and Google Apps Script webapps
- Loading large JSON datasets into BigQuery with Apps Script
- Logging differences in strings in Apps Script
- Measuring library load speed
- Migrating user and script properties
- Minimizing maps directionfinder api calls
- More client server code sharing
- More recursion – parents and children
- More sheet data fiddling
- Multiple inserts in Fusion Tables
- Namespaces in libraries and scripts
- Normalizing the header level of blank paragraphs
- Optimizing sheet formatting with Apps Script
- Optimizing showing and hiding rows and columns
- Organizing asynchronous calls to google.script.run
- Organizing parallel streams of server calls with google.script.run promises
- Parallel process orchestration with HtmlService
- Passing data to html service
- Patching site html
- Populating sheets with API data using a Fiddler
- Proxy jsonp
- Pseudo binding in HTML service
- Queuing asynchronous tasks with rate limit and concurrency constraints
- Recursive async functions
- Removing duplicate paragraphs
- Reporting file, function and line number in Apps Script
- Resumable uploads – writing large files to Drive with Apps Script
- Roughly matching text
- Serving apps script to JavaScript app
- Sharing code between client and server
- Shortcut for adding nested properties to a JavaScript object
- Simple server side polling
- Sorting Google Sheet DisplayValues
- Squeezing more into (and getting more out of) Cache services
- Styling Gmail html tables
- Summarizing emails to a sheet
- SunCalc
- TimeSimmer : An adjustable timer for apps that need to speed up or slow down time
- Transform dates for add-on transfer
- Transposing sheet data
- Traversing a tree
- Unique values with data fiddler
- Unnesting data to sheet values
- Untangling with promises
- Use Drive properties to find app files
- Use promise instead of callback for settimeout
- Using Advanced Drive service to convert files
- Using Apps Script for xml json conversion
- Using array formulas to improve performance
- Using crossfilter with Google Apps Script
- Using D3 in server side Gas
- Using es6 promises server side in Apps Script
- Using Es6 with Apps Script
- Using exponential backoff with github api – dealing with data “in preparation”
- Using Google sheets via Bigquery from Apps Script
- Using named locks with Google Apps Scripts
- Using promises to orchestrate Html service polling
- Using promises with apps script
- Using the Itunes API with Apps Script
- Using the slideshare API from Apps Script
- Using timing functions to get insight into Sheets
- Watching docs for changes
- Watching for server side changes from the client html service
- What JavaScript engine is Apps Script running on?
- Why Base64
- Zipping to make stuff fit in cache or properties service.
Why not join our forum, follow the blog or follow me on twitter to ensure you get updates when they are available.