You’ve created an awesome Google Apps Script web app for your secret society within your Google Workspace organisation or …dom! dom! DOM!Β … the world. The problem is that you only want to share your web app with the worthy. Those selected few. ππππ
How do you do this? How to prevent this most coveted of apps from reaching the wrong hands?
It’s actually surprisingly simple.
In this tutorial, we will explore how to validate selected users to provide access to your web app. For our example, we validate users based on whether or not they have edit access to a Google Drive file ( a common occurrence). In the discussion, we will also look at alternative ways of validating emails.
One of the bonuses of the approach we will go through is that it can also be easily adapted for use in Google Workspace Add-ons, and Editor Add-ons like sidebars and dialogue boxes.
We’ll start off with an example and then move to a quick-use guide for those of you who just want to get in and apply the code to your own project. Then for those who want to know how it all works, I’ll dive into the details.
Carelessly left behind Google Apps Script time triggers can be greedy little blighters that can ultimately end in a whole lot of noggin scratching when your scheduled scripts decide not to run all of a sudden. Then there is a whole lot of house cleaning to remove all those time triggers you couldn’t be bothered setting a calendar reminder to remove them when you didn’t nee them any longer.
After all, there are some pretty tight quotas for the Consumer account (90 min) and if you are doing some heaving lifting in your Google Workspace account (6 hrs), then it will add up fast. Well… maybe I am just a glutton for triggers.
In this tutorial, we will cover how to schedule weekly time triggers between a period of dates in Google Apps Script programmatically using the Clock Trigger method of the ScriptApp class. The code basically sets all the triggers up on the desired range of dates and then removes all the triggers when the time expires.
Let’s get stuck into it!
The Code
Triggers.gs
This is the main code you will copy and paste into your own project. Read the Quick Use guide for what you will need to update for your own project.
console.log("Copy and paste data to repository file.");
};
functionopenAndSend(){
// Share edit permission and send a group email to team to edit the sheet.
console.log("Share edit permission and send a group email to team to edit the sheet.");
};
functionsendDeadlineReminder(){
// Send a deadline reminder to users who have note completed the task 1 day before deadline.
console.log("Send a deadline reminder to users who have note completed the task 1 day before deadline.");
};
functionremoveEditorsFromSheet(){
//remove editors from sheet on deadline.
console.log("remove editors from sheet on deadline.")
};
Quick use guide
Here, we will quickly go through using the script to get you up and running.
The Example
In the example, we have an imaginary document that needs to be edited by our team. If you look at the ExampleFuncitons.gs file you can see our list of time trigger task for our Google Apps Script project as follows:
Refresh the Google Sheet: We possibly need to send a report and clear it out at a certain time to set it up for the next week.
Open and send an access email: We will send off an email to our team to let them know that they now have edit access for the sheet and complete their weekly task.
Send a deadline reminder: Just before the deadline, we send out a reminder to our stragglers that the deadline is due so that they can get unnecessarily offended. ππ
Remove the editors from the sheet: Once the deadline hits, we revoke edit access for our team from the sheet to maintain the integrity of the sheet before we send off our report.
Before you add your trigger times
A note on time zones
Just hold up one second. Before you add your trigger times, I recommend that you double-check the time zone assigned to your project.
Didn’t know that was a thing?Β No worries. Check out this short video on how to get that done fast:
I recommend creating a separate *.gs file to add in the timeTrigger object from lines 35-145. It just gets it out of the way of your awesome project codes.
The runsies() function is just an example function. If all you want to do is add your triggers days and dates in and hit run, then it is fine. But if you want to programmatically draw your start and end trigger dates and weekly trigger times from somewhere else then all you need to add into your own function is theΒ TIME_TRIGGER objects and then run:
timeTrigger.deploy(TIME_TRIGGER);
Adding your times and date window
TIME_TRIGGER object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//List of time trigger data that we want to schedule.
constTIME_TRIGGER={
// Set [year, month, day]
startDate:[2021,3,8],// or "now" if it is immeidate.
endDate:[2021,3,10],
setTimes:[
// [function, weekday, hour]
// e.g. ["runFunciton", "Sunday",20]
["refreshSheet","Monday",7],
["openAndSend","Monday",12],
["sendDeadlineReminder","Wednesday",7],
["removeEditorsFromSheet","Wednesday",15],
//You can add more triggers.
]
};
There are two parts to this object. First, set the start and end date that you want to run your weekly triggers.Β Lines 4 & 5
If you want to start your triggers straight away, then you can enter “now”, otherwise enter in a date. You will get an error message if your date is before the current date.
The end date removes all the time triggers in your project. So if you have other triggers, you will need to make some changes to the timeTrigger object (Maybe register each trigger id in PropertiesSerivce).
To add your dates, start with the year, month and date. Note that unlike the weirdness of the Javascript Date() constructor, I have made the month the common number. So a 3 will be March (not April in the Date() constructor).
Next, add the weekly triggers that you want to run each week for this project.Β Lines 21-28
You can add in as many as you want here. In the example, I have added four. Each piece of weekly trigger data is contained in an array:
[function, weekday, hour]
function: The function that you want to be triggered.
weekday: Full word days of the week from Monday to Sunday.
hour: The hour of the day that you want to run the trigger.
Deploying and scheduling the time trigger
To schedule, yourΒ triggers run the timeTrigger.deploy(TIME_TRIGGER); function.
If you want to test things before your first deploy, you can check the triggers are all set up you can check the triggers in your Apps Script menu (1).
To check the times of your trigger, you can click on the vertical ellipses beside each trigger (2).
You can delete all the triggers in your project and start again with:
timeTrigger.remove();
That’s pretty much all you need to know to get this script up and running in your project.
If you want to learn more about how the timeTrigger object was written and how to code Clock Triggers, jump down into the next header.
Need help with Google Workspace development?
My team of experts can help you with all of your needs, from custom app development to integrations and security. We have a proven track record of success in helping businesses of all sizes get the most out of Google Workspace.
* @param {Object} triggerData - a complext object containing star
* and end dates and times in the week to set.
*/
deploy:(triggerData)=>{
timeTrigger.setEndDateTrigger(triggerData);
//Check if start date is now
if(triggerData.startDate.toLowerCase==="now"){
timeTrigger.setTrigger(triggerData.setTimes)
return;
}
triggerData.startDate[1]-=1;// Minus 1 to get month index
let startDate=newDate(...triggerData.startDate)
let today=newDate();
today.setHours(0,0,0,0)
//Check if date is today.
if(startDate.getTime()==today.getTime()){
timeTrigger.setTrigger(triggerData.setTimes)
return;
}
//If in the future will schedule a date to start the weekly triggers.
else{
ScriptApp.newTrigger("timeTrigger.setTrigger")
.timeBased()
.at(startDate)
.create();
};
},
...
The timeTrigger.deploy() function is the main run function and takes the object of start and end dates and weekly triggers that we assigned in the TIME_TRIGGER object in the runsies() function.
Its first task is to set the date the weekly triggers need to be removed. This is done with the setEndDateTrigger(triggerData) function that we will discuss in a minute.
Check if start dat is now
Next, we need to check the input for the triggerData.startDate. If the user selected “Now”, then we immediately run timeTrigger.setTrigger() and complete the script.Β Lines 12-16
Check if start date is today
Alternatively, if the user puts in the current day’s date then we need to run the timeTrigger.setTrigger() straight away too. First, we need to transform the users triggerData.StartDate data into a readable date. We do this using the Javascript new Date() constructor that has the option to take the format; year, month, date:
new Date(year, month index, day of month)
Because the month index for Date()Β starts at 0 for January and ends at 11 for December, so we need to subtract 1 from the users month input before creating the date. Line 18
I used a Javascript spread operator (…array) to add in all the values of triggerData.StartDate which in our example are:
[2021, 3, 8]
So:
new Date(...triggerData.startDate)
Is actually, this:
new Date(2021, 3, 8)
Which is much tidier than:
new date(triggerData.startDate[0], triggerData.startDate[1], triggerData.startDate[2])
I’m really digging the spread operator lately.Β
Next, we need to compare the current date with the triggerData.startDate. We can do this by converting the two dates to times using the getTime() method. This transforms the dates into long number values that can be compared.Β Line 25
Before we do this though we kinda need to clear the current time out of the current data otherwise we won’t be able to compare things properly (line 26). When we run new Date() to create the today variable it will give us the date and the current time all the way down to milliseconds. However when we created the date for our scheduled date we only added the year, month and day so the time will be set to midnight.
Let’s update today date by changing the time of the day to midnight with:
today.setHours(0, 0, 0, 0);
If start date is in the future, schedule it!
Our final condition is if the user has scheduled a date in the future. We don’t want to clutter up their trigger quota unnecessarily so we need to postpone our triggers. To do this we ironically, need to create a clock trigger that runs only once on the date we want our weekly triggers to start.
To programmatically set a time trigger in Google Apps Script we call the newTrigger method in the ScriptApp class.Β Line 34
The newTrigger method takes our assigned function as an argument. This is the function we want to run when our trigger goes off. For us, this is the timeTrigger.setTrigger() function that will build the weekly triggers.
Your next step is to decide what type of trigger you want. In our project, we want a timeBased() trigger.Β Line 35
Selecting timeBased() takes us to the Clock Trigger builder, where we can choose from a bunch of settings. For us, all we want to set is a start date so we chose the at(date) method which takes a constructed Javascript date. Here we throw in the startDate variable we built on line 19.
Once we have all our trigger data inputted, we need to create() our trigger.
timeTrigger.setEndDateTrigger()
timeTrigger.setEndDateTrigger()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
/**
* Sets a trigger to deletes the assigned weekly triggers.
*
* @param {Object} triggerData - a complext object containing star
* and end dates and times in the week to set.
*/
setEndDateTrigger:(triggerData)=>{
//Set last day to remove triggers
triggerData.endDate[1]-=1;// Minus 1 to get month index
triggerData.endDate[2]+=1;// Move to following day
let endDate=newDate(...triggerData.endDate);
ScriptApp.newTrigger("timeTrigger.remove")
.timeBased()
.at(endDate)
.create()
},
...
The timeTrigger.setEndDateTrigger() function takes the triggerData as an argument. From that, it extracts the endDate array of year month and day.
Just like in timeTrigger.deploy(), we need to take one away from the month to get the proper value for Javascript. Then we want to remove our weekly triggers basically on the midnight of the following day after the user’s recorded end date. Lines 11-12
Why? Well, the end date usually means that it is a date the final trigger will occur on. We don’t want to remove our trigger before that date though or we will mess up our user’s process.
Once we have created our date we go through the same process as scheduling a start date in the future like we did in timeTrigger.deploy().
timeTrigger.setTrigger()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
...
/**
* Sets a weekly triggers for each setTimes item.
*
* @param {Array} times - 2d array each row containing trigger data
* such as: [nextFunct, weekday, hour, minute]
*/
setTrigger:(times)=>{
times.forEach((time)=>{
let[nextFunct,weekday,hour]=time
constwkDay={
Sunday:ScriptApp.WeekDay.SUNDAY,
Monday:ScriptApp.WeekDay.MONDAY,
Tuesday:ScriptApp.WeekDay.TUESDAY,
Wednesday:ScriptApp.WeekDay.WEDNESDAY,
Thursday:ScriptApp.WeekDay.THURSDAY,
Friday:ScriptApp.WeekDay.FRIDAY,
Saturday:ScriptApp.WeekDay.SATURDAY
};
ScriptApp.newTrigger(nextFunct)
.timeBased()
.onWeekDay(wkDay[weekday])
.atHour(hour)
.create()
})
},
...
This trigger is initialised from either the timeTrigger.deploy() straight away if the start date is the current date or is scheduled for deployment at a later date.
The timeTrigger.setTrigger() takes the 2d array of all the weekly triggers assigned by the user. In our example, that was this:
1
2
3
4
5
6
7
8
9
10
11
...
setTimes:[
// [function, weekday, hour]
// e.g. ["runFunciton", "Sunday", 18]
["refreshSheet","Monday",7],
["openAndSend","Monday",12],
["sendDeadlineReminder","Wednesday",7],
["removeEditorsFromSheet","Wednesday",15],
//You can add more triggers.
]
...
First, it loops through each set of trigger input data with a forEach loop.
Assigning variables using destructuring
I want to assign a variable for each item in the currently iterated array so that I can work with it in building the trigger. Here I used a destructuring assignment now available in Google Apps Script V8 runtime. Line 11
If you are unfamiliar with the destructuring assignment, you can basically set an array of variables – in our case – on the left and assign (=) them to a corresponding array of data on the right. So:
let [nextFunct, weekday, hour] = time
Would assign these values to the varialbes on the first iteration:
let [nextFunct, weekday, hour] = ["refreshSheet", "Monday", 7]
Without destructuring this variable assignment might look like this:
1
2
3
let nextFunct=time[0];
let weekday=time[1];
let hour=time[2];
Pretty cool, I reckon.
PReparing the days of the week
Next, we need to assign a day of the week to our clock trigger. These assignments require an enumerator which is basically:
ScriptApp.WeekDay.A_DAY_OF_THE_WEEK _IN_ALL_CAPS
Fortunately for us, we assigned our variable weekday to time[1] which is the day of the week for this iteration.
Lines 13-21
Creating the weekly clock triggers
Finally, we create our first clock trigger. Just like our date triggers we first call ScriptApp.newTrigger(nextFunct) where nextFunct is the time[0] value the user assigned as their function that they want to run on their trigger.
Again, we set the trigger to timeBased() but this time around we use onWeekDay() method. This method takes one of those weekday enumerators we store in our wkDay variable. We will select the correct one by adding weekday variable inside weekday:
wkDay[weeday]Β Line 25
For weekday triggers we can also set the hour of the day. This will deliver the trigger close to the hour selected.Β Line 26
The timeTrigger.remove() function runs on the end date. Alternatively, you can run this to remove all of your triggers if you are testing on making a mistake.
To remove your triggers, you will first need to get all of your projects triggers. We do this with ScriptApp.getProjectTriggers();Then, you will need to loop through each trigger and delete the trigger using the deleteTrigger() method that takes the trigger object.
Β Conclusion
I have been deploying triggers like this for a while now after and…erhm…unfortunate incident with meeting my quota and being lazy and leaving old triggers active. This now saves me a lot of future grief.
One thing you might have to adjust for a bigger project is the timeTrigger.Remove() method. If you have other triggers running in the same project, you probably don’t want to delete them. You could use the properties service to store your trigger ids and then check them against the list of all triggers to determine if they need to be deleted.
Another thing worth considering is that you might want to schedule multiple dates to run your triggers. You would need to loop through each start and end date here and create a trigger for each one.
Anyway, if you found this useful please give it a like in the comments below and if you have a plan to deploy it in your own project or a version of it I would love to hear about it in the comments.
Recently I raised a support ticket with a tech company I was subscribed to where we were trying to resolve an integration issue I had with their service. Once we had it all resolved they followed up with a feedback form. That feedback form just happened to be a Google Form.
Great, that’s cool. But that wasn’t what got me excited. They had exposed the raw URL link to the form in the email and I noticed that there were some references to my name, my support number and a few other things in the URL query parameters.
I clicked the link to the Google Form and, as expected, the Google Form appeared with these values prefilled into my form.
We this is a pretty cool convenience, I thought. How did they get all the query paths to each form item?
A couple of days passed and I had a chance to figure it all out.
In this tutorial, I’ll walk you through accessing the prefill tool in Google Forms. Then, if you are keen on doing some coding, we’ll create a little custom feedback form for unique users that we will deliver via email.
Let’s play!
Google Forms prefill tool
Accessing the Google Forms prefill tool
First, take a look at my example Google Form:
Go ahead and type forms.new in your Chrome browser address bar and create a few form items so you can play along.
Once you are done, got to the top right next to your avatar and you will see a vertical ellipsis. Give it a good old click.
A popup window will appear. Three items down and you will see the menu item, Get a pre-filled link. Go on, you know you want to click it. I won’t judge.
A new window will appear in your browser with a sample of your form. Go ahead and fill out any part of the form that you want to have prefilled.
We’ll fill out the first three items in our form. Here, take a look:
As you can see above I have added my name (Yagi the Goat), a ticket number (6047) and issue (Login – Passwords).
You might have noticed down the bottom left of the screen a grey box with the prompt, Prefill responses, and then ‘Get link’.
Go ahead and scroll down to the bottom of your form and click theΒ Get linkΒ button (1).
Then click the COPY LINK button in the grey bar (2).
Paste your link in a new browser tab and hit enter to check that the pre-fill is what you wanted.
If you are happy with the prefill results, then paste the pre-fill link somewhere safe for you to use later.
You should be able to see some of the pre-fill items in your URL that you added earlier. We’ll go onto this later if you are following along to the Google Apps Script portion of this tutorial.
At first, I was a little lost at the usefulness of using a standard static pre-fill for your Google Form. Surely not all people on your form will need to choose the same thing. I mean, you may as well leave it out of the form, right.
However, after a bit of noggin scratching, I thought that maybe you could use a static prefill like this for a standard response to help most users skip filling in unnecessary parts of the form while still making it flexible enough for the user to change the form if they need to.
When it does become an awesome tool is when you can use the URL generated and update fields to customise it for each user.
In the next part of this tutorial, we will do just that with the help of some Google Apps Script and then add our form to a custom email.
Create a custom prefilled form link and email it
In this portion of the tutorial, we are going to create a custom pre-filled form link by altering our copied pre-filled form link and then send a custom email to a user with their name and their own unique Google Form link.
The example
Let’s assume we have our very own tech support team. After we complete each ticket, our team are eager (yeah right!) to find out how well they performed in their support of the client.
The team stores each completed ticket details in a Google Sheet like below:
Looking at the image of the Google Sheet above, we only want to send an email to those clients whose checkbox in columnΒ I is unchecked – indicating that they haven’t received and email yet.
We then want to send an email to our users with a message and a link to our unique pre-filled Google form.
For example, our last user, Andrew Bynum, would get an email like this:
Then when Andrew clicked on the form link he would be navigated to his own pre-filled Google Form with the first 3 items filled in like below :
Next, you can see 3 occurrences of entry followed by a number (in red) then equals to the pre-fill input we added (in green). Note that if a prefill item has a space, it is replaced with a plus (+) symbol.
We start to write out our code we can replace these pre-filled inputs with a variable that can update for each user we send our form to.
Time to check out the code to see how we do this.
The Code
This is a pretty basic procedural code so we will simply pack it into one function. No need to go crazy here:
sendFeedbackEmail()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* Create an email with a prefill link to a Google Form
*/
functionsendFeedbackEmail(){
/** #############################################
* Globals
* ##############################################
*/
constSS=SpreadsheetApp.openById("1Jbgl82JbVVRhWWUs7JSVaDh2ai52DGD5Vs7XKF5Qq8s");//Update for your workbook
constSHEET=SS.getSheetByName("Ticket");// Update to your Google Sheet Tab
constROW_START=2;//After your header, where does your row start?
We need to first set up some main variables that we will reference in our project. First, we will get access to the Google Sheet that contains the ticket data for our clients – the TicketsΒ file we mentioned earlier – using the SpreadsheetApp class.
We then call the openById() method which takes one argument, the file id. This can be found in the URL and should look similar to the one in the example. This is then put in the SS variable. Line 10
Next, we need to get to the sheet tab our data is in. For us, this isΒ Ticket. So we reference this sheet tab name with our getSheetByName() method and store it in our SHEET variable.Β Line 11
We will want to indicate what row our user data starts because we don’t want to include our headers. Here we set our ROW_START variable to 2 because our first user is in row 2.
Getting data range and values
Our next task is to get the range of all the data we need to add our pre-fill values, emails and client name data along with our checkbox to see if we need to email that user. We may as well select all the columns and grab the last row.
To grab the full range of our data we use the getRange()method. Which can take many types of arguments, but for us, we want to give it 4 number values:
Row start
Column start
Number of rows
Number of columns
We’ll add our ROW_START in our…um…row start argument. Our column start in the first column. Then we grab the last row, which will likely change often by using the getLastRow(). This will update as new entries come in.Β We then subtract this by the row start and add 1 to exclude the header.Β Line 13
To then get the values of the range we use our new range variable and call the getValues() method. This will produce a 2d array of all the data on our sheet.Β Line 14
Keeping track of emails sent.
Our checkboxes in columnΒ IΒ keeps track of who we have sent our feedback form to. We will update the checkbox with a tick if we have sent the form using some code.
Before we jump into our loop through each ticket we need to keep track of where the boxes are unticked and where the row of unticked boxes finish. We do this by setting up an object to store untick start and end rows that we will preset as false and update as we loop through the rows.
1
let uncheckedBoxRange={start:false,end:false};
If you wanted to speed things up in a bigger Google Sheet you could store the start row in a Properties Service like in the post below, but that’s beyond the scope of this tutorial.
Looping through our data and setting up our column variables
Now that we have the values of our Google Sheet in our VALUES variable, we want to loop through the 2d array and set some variables to each column of data we want to use in our script. We use the forEachmethod for our loop here with the first argument being the array containing all the cell data in the row and the second one, the row index:
1
VALUES.forEach((row,index)=>{
Next, we need to assign some variables to each relevant row item that we will use in either our email or our pre-fill. To do this we will use some destructuring to cleanly build our variables:
The bolded items are the only columns we want to use. In our destructured variable assignment, we create an array of all the variables we want to use and put a blank comma space between the variables we don’t want to use.
Creating the first name variable
It’s kinda weird these days to address someone by their first and last name in an email greeting. Some people even find it a little insincere or annoying. So we might want to just stick to the more popular informal first name.
To get our first name, or fname, we use the Javascriptsubstringmethod to just get the first part of our string up to just before the first space. The substring method takes 2 arguments. The start position and end position. We find out the end position by using theindexofmethod that searches a string of text and if it finds the corresponding value, it will report the position of the value, but if the value does not exist it will report -1.
The resulting code would look like this:
1
name.substring(0,name.indexOf(" "))
Now, we are not certain if our users have put in a second name, or even have one for that matter. So if we just created our fname varaiable with this code we would get a weird error if we had a single name.
To fix that, we are going to use a ternary operator that we will first use to check if the name variable is a single name or not. Here again, we use the indexof method to check if there is a positive number. If so we will use the code above to generate our name. Otherwise, we will use just the name. Check out the full line of code:
1
let fname=(name.indexOf(" ")!==-1)?name.substring(0,name.indexOf(" ")):name;
Swapping spaces between words for “+”
When we create our custom pre-fills we noticed that spaces were repaced with plus symbols “+” in the URL. We want to keep the full name and the issues in our prefill and we know that both items potentially contain spaces in the text. To change the spaces to plus symbols, we will use the Javascript replace method with the help of a little bit of regular expressions.
The replace method takes two arguments, the item to search for and the item you want to replace it with. Because the item we are searching for is a space it’s good practice to use a regular expression rather that ” ” to be certain you catch it. Our regular expression looks like this:
1
/\s/g
The \s is the symbol for spaces. The two / mean anything between. The g is the symbol for global. So essentially this expression is saying that is is looking for any occurrence of a space all over (globally) in the string.
We’ll update the two original variables (which will upset the functional programming purists, but hey, it’s only a small bit of code) so our two lines will look like this:
1
2
name=name.replace(/\s/g,"+");
issue=issue.replace(/\s/g,"+");
Sending off our email
In the next section of our function (Lines 33-46), we check to see if we need to send an email, and if we do, we send it away with our pre-filled link to our form.
First, we use an if statement to check if the current feedback cell is false, then we are good to send the email.
Sendemail()
Next, we invoke the GmailApp Google Apps Script class and then use the sendEmailmethod. The sendEmail() method can take a few different argument structures, but I like to use the full method approach that takes the following:
Recipient:Β The email of the person you are sending your email to.
Subject: What your email is about.
Body: We’ll put in a placeholder here, “see HTML body” because we want to use HTML to make our email look fancy.
Options: The are a lot of options you can put inside the curly braces {} of this object, but for us, we just want to add htmlBody. Which allows us to add HTML to our email.
Let’s have a look at the sendEmail() method so far:
1
2
GmailApp.sendEmail(email,"Support Feedback","see HTML body",{
htmlBody:
The HMTL Email
We will use template literals to create our string of HTML text. Template literals start and end with backticks (`). If you want to add a variable into the string all you need to do is add ${your variable}. The other bonus is that you can happily put your string on new lines of your code without having to close and concatenate your string each time.
Let’s take a look at our htmlBody value:
1
2
3
4
5
6
7
8
9
10
11
`
<p>Hi${fname},</p>
</br>
<p>We are constantly trying toimprove our ability toprovide fast andhelpful support foryou.</p>
<p>Please takeamoment tofill out the Feedback Form below on how we did with your recent ticket:</p>
You can see that it all looks like pretty standard HTML text separated by paragraph tags <p> and breaks </br>. We’ve added in the first name (fname) in the greeting at the start and then created a link to our pre-filled form that we have customised with our variables.
Here is what each entry looks like:
entry.1046214884=${name}
entry.2009896212=${ticket}
entry.415477766=${issue}
Once this part is complete the emails are all sent off. Time to update our Google Sheet to show we have done this job.
Updating the checkboxes
The checkbox process occurs at the end in two stages here. First as we are iterating through our forEach loop we need to keep a record of the first unchecked box and the last one.
Remember earlier that we had set up the variable, uncheckedBoxRange, before we started the loop. Now we want to check if this is the first time we have found an unchecked box. If it is we want to update uncheckedBoxRange.start with the current index plus the ROW_START value to get the row number and also update the uncheckedBoxRange.end.
If we have already found the first occurrence of an unchecked box, we skip updating the start value and just update the end value.
1
2
3
4
5
6
7
//Update your checkbox range.
if(!uncheckedBoxRange.start){
uncheckedBoxRange.start=index+ROW_START;
uncheckedBoxRange.end=index+ROW_START;
}else{
uncheckedBoxRange.end=index+ROW_START;
}
Outside our loop, we then need to use our uncheckedBoxRange object values to update our checkbox columns in our Google Sheet.
First, we need to get the total number of emails we sent. We do this by subtracting the uncheckedBoxRange.end from the start and add 1.
1
let uncheckedCount=1+uncheckedBoxRange.end-uncheckedBoxRange.start;
We then want to create a string of true values equal to the uncheckedCount. This can be done fairly cleanly by the new Array constructor that can take an argument to generateΒ nΒ amount of values in an array.
Next, we use thefillmethod to identify what we want to fill each array value with. For us, this is a child array with the value true in each. Why a new array inside our main array? Because each row of a sheet is its own array.
1
let ticks=newArray(uncheckedCount).fill([true]);
We then use the Google Apps Script getRange() method again to select our range referencing our start row of unchecked boxes, column nine, the total number of unchecked boxes. We don’t have any other columns to worry about so we don’t need a fourth argument.
Finally, we use the setValues() method inserting our newly created array of true (or ticks) into our checkboxes.
Conclusion
To run your code from the Google Apps Script IDE simply click on run and follow the prompts:
Alternatively, you could set a time trigger to run your code daily or weekly or when the Google Sheet changes, or have a button or menu item that you click in your sheet to run the code.
So what do you think? Would you use pre-fill in your own project? I would love to hear how you applied custom pre-fill. It’s always interesting to see what creative things people develop.
Need help with Google Workspace development?
My team of experts can help you with all of your needs, from custom app development to integrations and security. We have a proven track record of success in helping businesses of all sizes get the most out of Google Workspace.
In this tutorial, we will cover creating a custom Table of Contents that lists your Google Sheets tabs on its own tab using Google Apps Script. This Table of Contents (TOC) will update whenever you open your sheet or choose to update it with a button.
But Yagi! Can’t I just check the list of tabs from the All Sheets button in the bottom right or scroll across bottom list of tabs until I find whatΒ I need?
Sure you can, but sometimes the sheet tab name just doesn’t properly explain what is in your sheet. There is a word limit to the tags and that bottom tab bar will get awfully cluttered if you start creating verbose tags. π
On most of your sheets, you will probably have a title or description perhaps on the first row. This will probably more accurately detail what is occurring. You might also have some universal details that you have on all your sheets that you want to display on your table of contents tab.
Finally, you might only want certain tabs to be in your Table of Contents.
Note!Β As always, read as much as you need or settle in to read the whole thing.Β
Features
Our code contains the following features:
Generate a table of contents on a separate sheet tab. Any time we create a new sheet tab it will be added to our table of contents either on the next load of the Google Sheet or manually when editors of the sheet click a button.
Sort the sheet tabs alphabetically.Β So that your users have an easily indexable list. The can be removed.
Dedicated ‘Notes’ Sheet Tab for you to easily edit to change how you want your Table of Contents to be displayed. Make changes to how you want your Table of Contents to look right inside your Google Sheet.
Choose the location cell of your tab titles. Assign what cell your titles are going to be in.
Identify what Sheet Tabs you don’t want to be included in your Table of Contents. You might not want to share all of your tabs, right? For example, it seems a little silly to share your Table of Contents tab.
Optional addition of your Sheet Tab name included in the TOC.Β
There will be a bunch of example Sheet Tabs already there for you. Just go to File > Make a copy.Β Then open the Google Apps Script Editor (Extensions > App Script).
The Code
Code.gs
Code.gs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//############# GLOBALS ##################
constNOTES_SHEET="Notes";
/**
* Google Apps Script trigger that runs each time the sheet is opened.
*/
functiononOpen(){
updateTOC();
};
/**
* Updates the Table of Contents (TOC)
*
* Searches the sheet tabs and if the shet is not on the excluded list,
* it will add it to the TOC.
* It will then publish it on the assigned page.
*
* Requires a dedicated "Notes" google sheet with a set of input boxes.
*
*/
functionupdateTOC(){
constSS=SpreadsheetApp.getActiveSpreadsheet();
constSS_ID=SS.getId();
constTOC_vars=getVariables(SS);// Get the object of values drawn from the 'Notes' sheet tab.
//Set the Table of Contents to active sheet (focus to this sheet)
Enter in all of your extra Sheet Tabs. Or as many as you have. You can always add more and your sheet will update your Table of Contents (TOC) next time the sheet opens.
Enter all of your parameters for your TOC (more on this in a bit)Β in the Notes sheet tab and click the button to run the code for the first time and go through the process of accepting permissions to run the code if you are happy with it.
Make a copy of the Notes tab data. Create a Notes tab and paste it into the exact same location.
Right-click on the Notestab of the Template Google Sheet. SelectΒ Copy to > Existing spreadsheet.Β Then search for the current Google Sheet you are working in.
Then copy the Google Apps Script code above and paste it into your code editor.
What if I want to put the Notes setup in another place?
If you want to put the setup data in another Google Sheets tap, you will need to update the NOTES_SHEET variable on line 2 of the Code.gs file.
1
2
//############# GLOBALS ##################
constNOTES_SHEET="Notes";
If you want to move the setup data to start at a different cell you will need to scroll down to the getVariables() function and update the following line:
1
2
3
...
.getRange("A1:B31")
...
Ensure that the range is 30 rows deep and 2 rows wide and you will be good.
Completing the Setup Data in the Notes Sheet Tab
All grey areas indicate the places you need to fill out. There are instructions for each part. If you need an example, hove over the input fields and a note will popup with an example.
1. Select the location of your Title
All of your sheets will probably have the exact same location of their Title. Here you will provide the cell. If the title is merged over multiple cells, select the first cell in the top-left.
An example of a valid input would be, A2 or B4.
2. Do you want to add the sheet tab name to your Table of Contents?
You can essentially choose to display your table of contents with a counter and the title:
Or include the Sheet Tab name as a third row.
Having the sheet tab name can be really handy if you want to create other columns of data for your Table of Contents using the INDIRECT Google Sheets function. Take a look at this example:
Here is the formula, have a try yourself if you are playing along:
=IF(C3="","",INDIRECT(C3&"!A2"))
Check out this example sheet where we have added the name and students who have grades remaining to the TOC.
3. When a TOC link is clicked where should we navigate to?
You can choose what cell you want your uses to be navigated to when they click the link in the TOC.
You might not always want your users to go straight to cell A1. Perhaps you want to get them to work straight away and navigate them to the first cell of the data they need to enter say, cell B6 for example.
4. Name the Sheet Tab Where you are storing your TOC.
This will automatically be set toΒ Contents, but you might want to call it TOC or list, or something.
Note that this will automatically update cell A20Β so that it is excluded from the contents. If you are feeling a little eccentric then you can delete this.
5. The start row of the TOC
Choose the row that your Table of Contents, including the headers, will go. You might want to give your contents sheet tab a title so you may wish to indicate row 2 here.
6. Excluding sheets
You can list all the sheet tabs you want to be excluded here. the TOC sheet and the Notes tab is in by default but you can add up to 12 sheets you want to be excluded.
This might be useful for hidden sheets or sheet that don’t follow the Title pattern.
7. Run the code
To generate the TOC for the first time, run the code and got through the permission process. you will only have to do this once.
If you add more sheet tabs you can either click the button again or reload the page.
That’s all there is to set up your own Table of Contents for your Google Sheet. If you want to dive into the code with me, head down to the next chapter. If you are happy with this free tool, hit the like button and subscribe. Finally, donations help keep this site alive and reduce the ads I need to put on here. If you want to donate and support me there is a button up in the top-right of the sidebar.
Need help with Google Workspace development?
My team of experts can help you with all of your needs, from custom app development to integrations and security. We have a proven track record of success in helping businesses of all sizes get the most out of Google Workspace.
Not much going on in the Global Variables. If you have your Table of Contents (TOC) setting in another sheet then you will need to update this.
onOpen()
onoOpen
1
2
3
4
5
6
/**
* Google Apps Script trigger that runs each time the sheet is opened.
*/
functiononOpen(){
updateTOC();
};
The onOpen() function is a reserved custom trigger in Google Apps Script.Β It can take one argument commonly notated asΒ eΒ for the event. For us, we do not need the event argument so we have left it out.
As its name suggests the onOpen() trigger runs when the document is first loaded. The function’s only task is to run the updateTOC() trigger.
It is generally a good practice to not bloat these custom trigger. Instead, use them to refer to functions that complete specific tasks.
updateTOC()
This is the main driving function. It will review the setup details for the TOC and then collect all the Google Sheet tab excluding the ones indicated. Then it will add the table of contents to the assigned sheet and then sort it.
Acquiring the main variables
1
2
3
4
5
...
constSS=SpreadsheetApp.getActiveSpreadsheet();
constSS_ID=SS.getId();
constTOC_vars=getVariables(SS);// Get the object of values drawn from the 'Notes' sheet tab.
We will need the unique ID of the spreadsheet to create our URL to link to other parts of the Google Sheet. Fortunately, we can do that easily with the getId() method.Β (Line 3)
Finally, we need to collect all the values that have been submitted in the TOC settings block found in the Notes. This is done with the getVariables(SS) function. This function takes the current spreadsheet object as an argument and returns an object containing something like the following example: (Line 4)
1
2
3
4
5
6
7
8
9
10
11
12
{
cellLoc:'A1',
sheetTabName:true,
navTo:'A1',
tocName:'Contents',
tocStart:'2',
exclude:[
'Contents',
'Notes',
'Example'
]
}
Loading the sheet on the Table of contents tab
1
2
//Set the Table of Contents to active sheet (focus to this sheet)
You’ll probably want your Google Sheet to open onto your Table of Contents each time. You can do this with the setActiveSheet() method that takes the sheet identifier.
Inside the brackets, you can see that we are using the getSheetByName() method to grab our selected sheet by calling on the TOC_vars object’s tocName key. In our example, we are referencing the Contents sheet tab.
If you don’t want the spreadsheet to open on your TOC you can comment this out or change the name of the sheet to your desired sheet tab name.
Set up the container variable that will store the TOC
1
2
3
4
5
6
...
//Set up headers depending on if user selects to add the Sheet tab or not.
let TOC_list=(TOC_vars.sheetTabName)?[["#","Title","Sheet Name"]]:[["#","Title"]];
let count=0;// This is to add a serial number to the list.
...
In our TOC setting, we give you the option to include the Sheet Tab Name as well as the title and reference number.
We use a Javascript ternary operator to first check if the tick box has been selected. If it has, we add the reference number, title and sheet name headers and store it in our TOC_list variable. If it hasn’t we only store the reference number and title headers. (Line 3)
To create our reference number, we will add a count variable and set it to zero. (Line 5)
Looping through all the Google Sheets
1
2
3
4
5
6
7
8
9
10
11
...
//Loop through all sheet tabs and select the title from each.
SS.getSheets().forEach(sheet=>{
let sheetName=sheet.getName();
if(!TOC_vars.exclude.includes(sheetName)){
let title=sheet.getRange(TOC_vars.cellLoc).getValue();
let sheetID=sheet.getSheetId();
...
Our first task is to iterate through all the sheet tabs. We can get a list of sheets using the getSheets() method. From there, we can apply the forEach JavaScript method to iterate through each sheet. (Line 3)
The forEach() method runs a function for each element in the array. We set sheet as our iterator variable.
The first task is to grab the sheet name from each sheet and store it in the sheetName variable. (Line 5)
As we look at each sheet name, we need to check it against our list of sheet tabs we want to exclude from our TOC. This is done on line 7 with an if statement that says that if the current sheet name is not included, or present, in our list of excluded sheet tabs, then continue with adding it to our table of contents.
We use the very fancy includes JavaScript method here to check if our current sheet exists in the list of excluded tab. Note the ! at the start which can be described as ‘not’ but more formally it means that we are looking for a false report on our if statement.
Next, we grab the title by using the getRange() Google Apps Script method to find the cell with the title in the currently iterated sheet. The location of the title is drawn from the TOC_vars.cellLoc value. The getRange() method can take, among other arguments A1notation to find a range. In our example, this is cell A1.
Lastly, we grab the sheet id. We will use this in a moment to create our sheet tab link.
Creating the link URL to each sheet tab
1
2
3
4
5
...
//Get the link to the sheet.
let sheetCellURL=`https://docs.google.com/spreadsheets/d/${SS_ID}/edit#gid=${sheetID}&range=${TOC_vars.navTo}`;
let hyperlink=`=HYPERLINK("${sheetCellURL}","${title}")`;
...
We’ll be making use of the Google Sheet HYPERLINK function to create a link for the title for each sheet. This function takes two arguments. The URL and the label for the URL. (Line 4)
Above this line, we will build the URL. There are three key points that we make modifications to the URL that you can see in the curly braces (${}).
The SS_IDis the unique spreadsheet ID for the current document.
The sheetID is the unique ID number for the sheet tab.
The TOC_vars.navTo is the cell where we want to direct the user to in the sheet.
Adding the count, title/link (and sheet name)
1
2
3
4
5
6
7
8
9
10
...
count++
//Add sheet tab data and count depending on whether user chose to add the Sheet Tab name
if(TOC_vars.sheetTabName){
TOC_list.push([count,hyperlink,sheetName]);
}else{
TOC_list.push([count,hyperlink]);
};
...
After we first increase our count by one (Line 2) we then need to add the count, the title connected to our link and if we chose to add the sheet name, well… we add the sheet name.Β π
Line 5sif statement checks if the user selected the sheet tab name. If they did we push the count, hyperlink and sheet name to the TOC_list. Otherwise, we just push the count and the hyperlink. (Lines 5-9)
We will soon be pasting in our table of contents, but first, we will need to determine how deep our data is in rows and how wide it is. (Lines 6 & 7)
Just in case you delete out some Sheet tabs we want to make sure that you have a clean page, so we initially clear out the content. First, we grab the range with getRange() this time using 4 number parameters: (Line 8)
Row Start
Column Start
Row height
Col width
We have made the row height 100. It would be rare that you had more than 100 sheet tabs worth of rows in your TOC but you can always update this. Google is vague about the limit of sheet tabs.
Then we append the clearContent() method that clears the data from the range but not the formatting.
Finally, grab the range of the Table of Contents sheet again this time using our row height gathered from the length of the array. We then use setValues() to input our array of TOC into our sheet.
Our last task is to sort our table of contents. This is an optional step and you can comment out these two lines if you don’t want to use it.
We want to make sure that our data is loaded into our Google Sheet before we sort it or we might have an error or the sort might be skipped entirely. This is called accounting for Race Conditions. This is done by applying the flush() method straight from the SpreadsheetApp class. (Line 2)
Next, we want to grab the row below our newly added header and then all the listed sheet tabs. We add the Google Apps Script sort() method to this which for us takes a single argument, sort ascending by the 2nd across. (Line 3)
getVariables()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Acquires the user preferences for the TOC from the 'Notes' Google Sheet.
*
* @param {object} SS - the SpreadSheet App object.
* @return {object} = an object containing.
*/
functiongetVariables(SS){
constvals=SS.getSheetByName(NOTES_SHEET)
.getRange("A1:B31")
.getValues();
//Assign input from 'Notes' tab to keys.
constdataReferences={
cellLoc:vals[3][1],
sheetTabName:vals[6][1],
navTo:vals[9][1],
tocName:vals[12][1],
tocStart:vals[15][1],
exclude:(()=>{
return(vals.slice(19)
.map((row)=>row[0])
.filter((row)=>row!==""))
})()
}
returndataReferences;
};
The getVariables() function takes the spreadsheet as an argument and returns an object, for example:
1
2
3
4
5
6
7
8
9
10
11
{
cellLoc:'A1',
sheetTabName:true,
navTo:'A1',
tocName:'Contents',
tocStart:'2',
exclude:[
'Contents',
'Notes',
'Example'
]}
The functions first task is to grab the range of Table of Contents settings data. First, it grabs the sheet by its name (Line 8).
Then it grabs the range. You can change this range value if you put the settings range in a different spot. Just make sure it is 2 columns wide and 30 rows deep. (Line 9)
Next, we grab the values of the settings range with the getValues method. (Line 10)
We then create the dataReference object and assign our setting values to our sheet. Each location is in a 2d array and we draw them out of our vals array by first referencing the row and then the column: (Lines 13-23)
vals[row][column]
To get our list of excluded sheet tabs we run an Immediately Invoked Function Expression (IIFE)(Line 19). First, we slice our vals array from row 19 onwards (Line 20). We thenΒ use the map method to iterate through the remaining rows selecting only the first column (Line 21). Finally, we filter out all the empty rows (Β Line 22)
The dataReference object is then returned to updateTOC() function.Β Line 25
Conclusion
Creating a table of contents in a tab of your Google Sheet is pretty useful for your users to be able to quickly navigate to what sheet tab they need. I hope that after reviewing the code you can make some changes for your own project.
If you have been playing along, you might have noticed that there is no data validation to ensure the received TOC settings are correct. I kinda thought adding this extra level of complexity would detract from whatΒ I was trying to achieve in the tutorial portion of this post.
However, running some validation either Google Sheets-side with Data Validation or inside your Google Apps Script will help reduce errors, but to be honest, not many folks are going to have access to the settings and those that do will probably figure out the error.
I was compelled to write this post based on interest in my Table of Contents from my previous post on using Google Sheets as a recipe folder. Check it out:
I would love to hear how you applied this Table of Contents creator in your own project. Feel free to comment below.
If you like this tutorial, give it a like so I know to keep em coming. If you want a regular dose you can subscribe down below. And if you want to support me, feel free to donate (top right-sidebar).
Google Apps Script: SpreadsheetApp,Β addEditor/s, removeEditor/s, alert. Javascript: try…catch, forEach, join, push, template literal. Google Sheets
In this tutorial, we will go over the basics of adding users as Editors to Google Sheets with Google Apps Script. We’ll go through the process step-by-step, starting with two very basic codes and then progress on to error handling so your code doesn’t break for your user.
In Google Sheets just like Docs, Slides, Forms and Sites you can add co-editors to work on your projects. This is usually done straight from Google Drive or within the chosen Google file in the top right with the Share button.
The rules for sharing a specific user as an editor are pretty simple. The user must have either a Gmail (name@gmail.com) account, GSuite for Education domain account (name@yourdomain.com) or Google Workspace (formerly, Gsuite) account with an email in the workspace’s domain (name@yagisanatode.com).
Now that we have all the basics, let’s go ahead and write some Google Apps Script code. First of all, open a Google Sheet. It can be one that you want to use to add and remove editors with code on a project you are working on or just a practice Google Sheet. Then go to Extensions > App Script.