Web App Development with Google Apps Script/Printable version
This is the print version of Web App Development with Google Apps Script You won't see this message or any elements not part of the book's content when you print or preview this page. |
The current, editable version of this book is available in Wikibooks, the open-content textbooks collection, at
https://en.wikibooks.org/wiki/Web_App_Development_with_Google_Apps_Script
Why GAS
Javascript everywhere
editUsing Google Apps Script is nice because it's javascript on both the front and backend. Normally[1] we expect javascript and html on the front end (meaning how our users will interact with our tool) but often you have to learn and use a different language on the backend, where you interact with your data and figure out what you need to show your users. Backend software examples include PHP, Ruby, and interesting flavors of javascript like node.js. With Google Apps Script you do the same sort of programming everywhere.
Web editing
editWith Google Apps Script can do all your editing in a browser. Your scripts are either tied to a spreadsheet or they're stand alone scripts in your Google Drive. Either way you launch the editor through drive and you get a decent, if not fantastic, development environment that you can use on a Chromebook.
Most other web development approaches encourage you to code locally on your own machine and then push your code to the server that hosts the web page. Many people use GitHub to help facilitate the pushing, but getting a nice coding environment on your local computer can be a hassle so it's nice with Google Apps Script that you can just grab any web-connected computer to do your work.
Spreadsheets as database
editWhat I'll be talking about mostly in this book is using simple Google Sheets to store all your data. I've done a lot of web app development using a mysql-type backend database, and I really appreciate the speed and utilities those provide. But they're a pain to set up and they're a pain to interact with separately from your web application. Google Sheets, on the other hand, are quite easy to set up and have a great user interface already for you to go look at and mess with your data.
User authentication
editAssuming you're in a situation where you're programming in a Google environment (like a Google school like mine where all emails are just rebranded gmail accounts) there's a very simple command to determine the user's email address and from there you can decide what you want them to have access to.
References
edit- ↑ test footnote
Hello world
Create a new script
editYou can create a new Google Apps Script the same way you'd create a new Google Doc or Google Sheet: Using the "New" button scroll down to "Google Apps Script" under "more". That will open a new script window with a simple test function in it:
function myFunction() {
}
If you write some code in that function you can run the function with the buttons at the top. Typically to see any results you'd have to use Logger.log(some variable here)
.
If you put javascript code outside of a function it will also execute whenever you run any of the functions. Keep that in mind as we develop web apps. Anything outside of a function will run every time the page is accessed or you run a server function with an ajax call.
Set up doGet()
edit
To actually have a web app you have to do two things:
- Make sure you have a
doGet()
function in your script, and - Publish your site
Almost always you're going to want to write your html in a separate file (still housed in the overall script google entity) and then have the doGet()
function call it and return
it. Assuming you've created an html file called "main.html" you would call it like this:
function doGet(){
var t=HtmlService.createTemplateFromFile("main"); // note that you don't have to put the ".html"
return t.evaluate();
}
You would then access the page by going under to the "Publish->Deploy As Web App..." menu item. Once it's published you can access the url from that same menu item.
Templated html
Putting variables into html pages
editAs noted in the Hello World chapter, you can have a basis of an html page by creating one and using it as a template. Normally you'd want to tailor that page with information from the server (ie usually one of your Google Sheets). You can use the various templating tools to do that.
You send a variable to the template with t.coolvariable="whatever"
and you put it in the html document with <?= coolvariable ?>
.
doGet code
editfrom "code.gs":
function doGet() {
var t = HtmlService.createTemplateFromFile("main");
t.coolvariable = "hi there";
return t.evaluate();
}
html code
editFrom "main.html":
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<?= coolvariable ?>
</body>
</html>
Web result
edithi there
More complex variables
editIf you want to send something more complex than a string, there are a few options:
<?!= somevariable ?>
will print things with special characters in it. This is useful, for example, for strings that contain html tags in them.var pagevariable = <?!= JSON.stringify(somevariable) ?>;
inside a<script>
tag on the html page will then let the local javascript deal with it as a global variable.- If you have a bunch of variables that you want to have as global variables in the html document:
t.globals={"var1":var1, "var2":var2};
in the code.gs docvar globals = <?!= JSON.stringify(globals) ?>; Object.keys(globals).forEach(key=>window[key]=globals[key]);
- this works because the
window
object is the global so-called "namespace" for the html document. That just means it's where global variables sit.
Passing functions
editThe Google Apps Script editor doesn't work very well when checking for syntax errors in javascript code that's embedded in an html file. Here's a trick to let you write javascript functions in the server code spaces (which lets them be syntax checked) and then pass them to the client. The trick is to use function expressions.
In the server side you'd do this:
function doGet() {
var t=HtmlService.createTemplateFromFile("main");
t.funcs=[a,b,temp];
t.funcnames=t.funcs.map(f=>f.name);
return t.evaluate();
}
var a=(c,d)=>c+d;
var b=(e,f)=>e*f;
var temp=(a,b,c)=>{
var d=a+b;
var e=b*c;
return d+e;
}
and in the client html file you'd do this:
var funcnames=<?!= JSON.stringify(funcnames) ?>;
var funcs=[<?!= funcs ?>];
funcnames.forEach((fn,i)=>window[fn]=funcs[i]);
Data from spreadsheets
Structure overview
editGoogle Apps Script lets you have access to Google Sheets. You do it by making a connection first to the Google Drive Sheet. There are two ways to do this:
ss = SpreadsheetApp.openById(id)
where "id" is the typical google drive id (the long string of characters usually right after "https://docs.google.com/spreadsheets/d/" in the url of a sheet).ss = SpreadsheetApp.getActive()
if your script is tied to a Spreadsheet (see below)
Once you have the spreadsheet variable (ss
in those examples), you can access the various sheets (some people call them tabs) in that doc and any data inside them.
Script tied to spreadsheet
editIf you want you can tie your script to a spreadsheet. Then the script doesn't live as a stand-alone google drive file but rather is stored with the Google Sheet. You can access the script by going to Tools->Script editor. It'll then look just like any other script. Oddly it still needs a name, but it'll prompt you to name it when you try to run something.
If you have it connected to a spreadsheet you definitely want to use the second approach mentioned above. Interestingly, the first method always works. There are some subtleties to access control when you tie it to a spreadsheet. Personally I do this a lot because if I want to edit the script I usually want access to the data at the same time. This way they're all stored together.
Read the data from a spreadsheet
editIf you have the spreadsheet variable, let's say you wanted to get all the data in a sheet called "Sheet1", which is the default name for the first sheet in a brand new Google Sheet document. You'd do that like this:
var sheet = ss.getSheetByName("Sheet1");
var data = sheet.getDataRange().getValues();
// then do something with the data
This usually takes a second or two, even for large (500+ row) spreadsheets. Once you have the data you can do all sorts of things. Let's say you have a spreadsheet that looks like this:
Name | age | |
---|---|---|
Andy | andy@myschool.edu | 48 |
Ash | ash@myschool.edu | 20 |
Char | char@myschool.edu | 18 |
Assuming it's in "Sheet1", you could augment the above code to create an html list to be displayed on the main.html page like this:
data.shift(); // this gets rid of the top row (the one with the headers in it)
var html = "<ul>";
html += data.map(r=>`<li>${r[0]} (${r[1]}) is ${r[2]} years old`).join('');
html += "</ul>";
t.html=html
Responding to users
JavaScript for user interfacing
editThis book is all about leveraging your google account to make data-driven web applications. It will not cover how to create your user interfaces very much. In this section I'll just give some simple examples, some of which don't even follow best practices but are quick and dirty approaches you can try for some proof-of-principle approaches.
User clicked something
editNearly any element in html can have an onClick
function added to it. This includes:
- buttons
- text (most easily done with
span
tags) - List items
onClick
is not usually considered the proper way to do things, but if you're sure it's the only thing you want that element to do when the user is interacting it can work for you.[1]
Here's some simple html/javascript code that would open an alert saying "you clicked me" when the user presses a button:
<button type="button" onClick="openAlert('press me')">press me</button> <!-- note the use of both types of quotes -->
<script>
function openAlert(text){
alert(text);
}
</script>
Update some text on the page
editAlerts are annoying, don't use them. Instead have the user's actions change something on the page. Here's an update to the last example where now pressing the button puts some text on the page just below the button:
<button type="button" onClick="openAlert('press me')">press me</button> <!-- note the use of both types of quotes -->
<div id='emptyatfirst'></div>
<script>
function openAlert(text){
document.getElementById('emptyatfirst').innerHTML=text;
}
</script>
If you plan to make lots of changes to the text inside that div
, you might instead want to make a global variable like this:[2]
<button type="button" onClick="openAlert('press me')">press me</button> <!-- note the use of both types of quotes -->
<div id='emptyatfirst'></div>
<script>
// here's a global variable:
var emptyatfirst=document.getElementById('emptyatfirst');
function openAlert(text){
emptyatfirst.innerHTML=text; // this uses the global variable
}
</script>
Footnotes
edit- ↑ The proper method is to do is
element.addEventListener('click', function() { /* do stuff here*/ }, false);
- ↑ Note that you can't do
var emptyatfirst=document.getElementById('emptyatfirst').innerHTML;
even if you thought that would save you some typing later.
Data to spreadsheets
Basic gist
editIf you collect some data from a user (maybe from an input
tag or something) that you want to save to your spreadsheet, you have to send it to the server-side, even though you're using the client-side for all the user interface.
The steps are:
- Grab data from the html element that the user adjusts
- Possibly massage the data a little (trim any trailing or leading spaces, for example)
- use the magic sauce:
google.script.run
! - Deal with any response from the server
Get data from input
editHere's a simply way to get the data from a textarea element when the user hits "send":
<textarea id="mytextarea"></textarea>
<button type="button" onClick="grabtext()">send</button>
<script>
function grabtext(){
var text = document.getElementById('mytextarea').value; // note you don't use innerHTML
// here's where'd you do something with that text
}
</script>
Sending to the server
editOnce you have some information to send to the server, you have to somehow get it there. That's where the magical google.script.run
comes in!
Basically you can send some data to the server by calling a function **on the server** that can do stuff there and then return some response.
Here's an example that builds on the one from above. We grab whatever the user enters and we add it to a sheet on the (attached) spreadsheet:
<textarea id="mytextarea"></textarea>
<button type="button" onClick="grabtext()">send</button>
<script>
function grabtext(){
var text = document.getElementById('mytextarea').value; // note you don't use innerHTML
google.script.run.addRowToSheet(text);
}
</script>
Meanwhile on the server (so in code.gs instead of javascript added to main.html):
function addRowToSheet(s) { // you can call what's passed anything you want. You don't have to call it "text"
var ss = SpreadsheetApp.getActive(); // grabs the associated spreadsheet
var sheet = ss.getSheetByName("Sheet1");
sheet.appendRow([s]); // appendRow needs an array like ["first column info", "second column info", ...]
}
Hearing back from the server
editOften you'll want to hear something back from the server. That's what withSuccessHandler
is all about. Basically you tell javascript both the server function you want to run **and** the client-side function that should run when something comes back. The syntax is weird, but it works.
Here's an example that augments the last one letting the user know the data has been saved in the spreadsheet:
<textarea id="mytextarea"></textarea>
<button type="button" onClick="grabtext()">send</button>
<div id="callthiswhateveryouwant"></div>
<script>
function grabtext(){
var text = document.getElementById('mytextarea').value; // note you don't use innerHTML
google.script.run.withSuccessHandler(dealWithIt).addRowToSheet(text);
}
function dealWithIt(returnvalue){ // you can call the return value whatever you want. Often you'll see people calling it "e"
document.getElementById('callthiswhateveryouwant').innerHTML="From the server: "+returnvalue;
}
</script>
Meanwhile on the server (so in code.gs instead of javascript added to main.html):
function addRowToSheet(s) { // you can call what's passed anything you want. You don't have to call it "text"
var ss = SpreadsheetApp.getActive(); // grabs the associated spreadsheet
var sheet = ss.getSheetByName("Sheet1");
sheet.appendRow([s]); // appendRow needs an array like ["first column info", "second column info", ...]
return "holy cow it worked!";
}
authentication
Why authenticate?
editQuite often you'll want to protect your web app to ensure only certain people can use it. When you publish your page you have a few options in two major categories:
- Who has access (who can get to the page)?
- Just you
- Anyone in your domain (hamline.edu for me)
- Anyone
- Which account is being used to run the script?
- Yours (even when other people access)
- Theirs (won't work with "anyone" above)
Those are useful but they're a little coarse grained. Sometimes you'll want only certain people to either have access and/or be able to do certain things.
Who is accessing?
editFor this section we'll assume you've set "who has access" to "anyone in your domain." If you do then you can determine who is accessing like this:
var email = Session.getActiveUser().getEmail();
Are they allowed?
editOnce you have the user's email, you can check it against a sheet in your spreadsheet that has allowed users and perhaps other information about them, like what things they're allowed to do. Let's assume you have a sheet that looks like this:
name | role | |
---|---|---|
arundquist@hamline.edu | Andy Rundquist | admin |
test1@hamline.edu | Testy McTesterson | student |
test2@hamline.edu | Tess Tesserson | faculty |
Then we could authenticate the user like this:
var email = Session.getActiveUser().getEmail();
var usersData=SpreadsheetApp.getActive().getSheetByName("my users").getDataRange().getValues();
usersData.shift() // gets rid of headers row
var user = usersData.find(r=>r[0]==email);
if (!user) {
return HtmlService.createHtmlOutput("sorry, nothing for you here");
}
// now do something cool with the user
var role=user[2]; // not 3, remember how array numbering works
if (role == 'admin') {
// do cool things here
}
// etc
syntax
ajax
editAjax stands for "Asynchronous JavaScript and XML" and it's the ability for a web page to communicate with a server without reloading the page.
To do it in Google Apps Script you use the google.script.run
tool. This command, which has several varieties, runs JavaScript code on Google's servers and potentially returns information back to your web page.
For example, if you wanted to send a name to a server, then check if it's in your spreadsheet and return true if it is and false if it's not that you can set to a variable, you'd do this:
First the server side code:
function doGet(){
var t=HtmlService.createFileFromTemplate("main");
return t.evaluate();
}
function runOnServer(name){
return checkInSpreadsheet(name); // this would be a function you'd write to check the spreadsheet
}
Now the portion of "main.html" with the JavaScript you'd be doing
google.script.withSuccessHandler(dealWithReturn).runOnServer("Andy");
function dealWithReturn(e){
if(e) {
alert("yep that name's in there");
} else {
alert("nope, that name's not in there");
}
The flow is: main javascript -> function on server -> returns data to your SuccessHandler -> your successHandler does something.
Array numbering
editA simple (and quite forgivable) mistake is to be off by a row or column when moving data back and forth between google sheets and javascript is the array numbering issuer. Javascript starts numbering at zero while google sheets starts numbering at 1. Here's an example:
var coolArray=[1,2,3,5,7,11]; // why not 9? because it's not prime!
var myCoolSheet = SpreadsheetApp.getActive().getSheetByName("my cool sheet");
// this is a dumb way to put data into a sheet but it points out the numbering difference
coolArray.forEach((item,i)=>{
// myCoolSheet.getRange(i,1).setValue(item); this fails
myCoolSheet.getRange(i+1,1).setValue(item); // this works
}) // don't forget to close everything
filter
editIf you need to filter an array, use, um, filter
:
var people = [{name: "Andy", age: 48}, {name: "Ash", age: 20}, {name: "Char", age: 18}];
// get people under 30:
var youngPeople = people.filter(person=>person["age"]<30) // note that "person" didn't need to be initialized
As with find, forEach, and map you can also use the index if you want:
var people = [{name: "Andy", age: 48}, {name: "Ash", age: 20}, {name: "Char", age: 18}];
// get people under 30 but not those with an index less than 2
var youngPeople = people.filter((person,i)=>person["age"]<30&&i>=2) // note that "person" didn't need to be initialized
find
editIf you need to find an element in an array, find
is super helpful:
var array=[{name: "Andy", age: 48}, {name: "Ash", age: 20}]; // an array of objects
var ashObject = array.find(person=>person["name"]=="Ash"); // returns the first element that gives "true" for the check
forEach
editIf you need to loop through an array and do something with every element, you could of course do this:
var array=["apple", "orange", "banana"];
for (var i=0; i<array.length; i++) {
doSomethingCool(array[i]);
}
But you could also do this, which I think reads better:
var array=["apple", "orange", "banana"];
array.forEach(fruit=>doSomethingCool(fruit));
Note that you never have to initialize the "i" variable this way.
If you need to access the item (fruit) and the location (index) that it's in the array:
var array=["apple", "orange", "banana"];
array.forEach((fruit,i)=>doSomethingElseCool(fruit,i));
Note the parentheses around fruit, i
that makes that magic work. Also note that you still don't have to declare the i variable!
includes
editIf you're looking to see if an array includes a particular element, try includes
:
var myArray=["apple", "orange", "banana"];
if(myArray.includes("green beans")) {
// do something to say that green beans isn't in the array
}
map
editSimilar to forEach, map
is a great way to create a new array based on an existing array. Let's say you wanted to add a tag around some text in an array:
var array=["apple", "orange", "banana"];
var newarray = array.map(fruit=>`<b>${fruit}</b>`)
You can also have easy access to the index of the item in the array:
var array=["apple", "orange", "banana"];
var newarray = array.map((fruit,i)=>`${fruit} is at index ${i} in the array`)
sort
editSorting arrays is quite valuable and sometimes a pain. Assume you have a 2D array from a spreadsheet with columns of "name", "age", "email". If you want to sort them by age, do this:
var data=... // data from spreadsheet
data.sort((a,b)=>a[1]-b[1]);
really sort needs to return a true or false when determining who should go first, but by subtracting two numbers, if you get a zero or a negative number it'll be treated as false whereas a positive number will be treated as true.
If you wanted to sort by the age and the name (alphabetically) you could:
var data=... // data from spreadsheet
data.sort((a,b)=>(a[1]>b[1])&&(a[0] < b[0]))
exercises
Javascript exercises
editArrays
editCould probably fit on one line of code If you have an array of [1,3,5,9] and want to produce an array of all those numbers squared, how would you do it? |
grab column Assuming you have a 2D array (like the rows and columns of a spreadsheet), how would you produce a variable with just the third column? |
every other row How could you grab every other row of a 2D array? |
Row filtering How would you get only rows where column A is bigger than column B and column C is one of the following: ["hi", "there", "you", "cool", "person"] ? |
Gracefully find a row How would you look to see if a row has your email in any of the cells and return either your row or a note saying it couldn't be found? |
Count rows that match Write code that allows the rows to be searched and returns the number of rows that match |
Produce table row from two sheets Assume you have a data sheet that lists the user's email. You also have a user sheet that gives more detail about each user. Return the html table syntax for each row where the data values are listed along with the full name of the user in the same row. |
Objects
editArray to object Given an array like ["apples", "oranges", "bananas"] return an object like {"apples":0, "oranges":1, "bananas":2} |
Accessing elements Given this object: {"name":"Andy", "likes":{"sports":["soccer","soccourt","softball","mountain biking"], "food":["hummus","desserts"]} } how would you address "mountain biking"? |
GAS exercises
editRegex exercises
editData structure exercises
editBlog site What structure would you need for a blog that has allowed users, some who can post, and all who can comment? |
templates
Templates
edit- Google Apps Script
var ss=SpreadsheetApp.getActive(); var sheet=ss.getSheetByName("test"); var data = sheet.getDataRange().getValues();
var x=100;