This also works on Docs – see Watching docs for changes
Here’s an example of how you would use it.
- create a watcher
var dataWatch = ClientWatcher.addWatcher ();
- Use it. It will call you every time there are any changes of data or selections in that active sheet.
dataWatch.watch (function (current,pack,watcher) { // do something - the latest data is in current.data , and the latest active selection information is in current.active. });
It really is that simple, and this will allow your add-on to react to changes that are happening server-side right away. Of course, there’s a lot going on behind the scenes that I’ll cover later, but first, let’s look at what gets passed to you.
Callback arguments
Your callback passed to the .watch() method would typically react to server side changes – perhaps visualizing the updated data or using the active range is some way. You receive 3 arguments each time a change happens in the sheet.
current
This is the current status of the data in the sheet server side. Its contents depends on what you’ve asked to be watched – we’ll look at how to do that later. Assuming you’ve asked to monitor data and selection changes, it will look like this.

Property | if monitoring has been requested, can contain – |
active | information on the active selection |
data | The [[]] array for the property being monitored (eg Values, Backgrounds etc) |
fiddler | A fiddler object with the data structured (if requested). Will only exist for the Values property. |
sheets | An array of sheet names |
Pack
This is not normally required, but the pack object contains info on what has actually changed. For example, if you are monitoring both data changes and active changes you can test pack.changed.active and packed.changed.data to see which (maybe both) has changed to provoke your function being called. If you are monitoring sheets, then packed.change.sheet will indicate there has been a change in the number of sheets or their names.
watcher
This is the watcher object and is passed for convenience. You can use it to get more information on the polling process. For example, watcher.getStatus() will provide information on how many polling requests there have been, hot many resulted in changes, and even how much time its spent waiting – a good measure for checking the latency of client/server requests. Just divide the wait time by status.serial and you’ll get the average latency.
{ "serial": 136, "requested": 1464524280134, "responded": 1464524280749, "errors": 0, "hits": 2, "totalWaiting": 81312 }
Some useful methods available via the watcher object
Method | Purpose |
getWatching | What is currently being watched. Can be used to change monitoring behavior midflight. |
poke | Reset all the checksums to refresh all data being watched. Recommended if you change properties being watched midflight |
watch | start watching |
stop | stop watching |
restart | restart watching after a stop |
getCurrent | Get all current values. This is the same as the ‘current’ argument passed to your callback |
getStatus | Get status information on the polling process |
Demo
You can try all this out using the demo. I’m using my usual layout for HTMLservice so I have more files than you would need to for such a simple demo, but you should find it a useful starting pattern for more complex apps as described in More client server code sharing
Setting up a watcher.
These are the default values – this will monitor for data and selection changes in the activesheet.
{ pollFrequency:2500, // if this is 0, then polling is not done, and it needs self.poke() id: '' , // Watcher id pollVisibleOnly:true, // just poll if the page is actually visible watch: { active: true, // whether to watch for changes to active data: true, // whether to watch for data content changes sheets:true // watch for changes in number/names of sheets }, checksum:{ active:"", // the active checksum last time polled data:"", // the data checksum last time polled sheets:"" // the sheets in the workbook last time polled }, domain: { app: "Sheets", // for now only Sheets are supported scope: "Sheet", // Sheet, Active or Range - sheet will watch the datarange range: "", // if range, specifiy a range to watch sheet: "", // a sheet name - if not given, the active sheet will be used property:"Values", // Values,Backgrounds etc... fiddler:true, // whether to create a fiddler to mnipulate data (ignored for nondata property) applyFilters:false // whether to apply filters } }
You can override these changes by passing an options object when you add the watcher. For example, here is one that watches for changes in background colors, but does not monitor for changes in active selection.
var colorWatch = ClientWatcher.addWatcher ({ watch:{active:false}, domain:{property:'backgrounds'} });
Running multiple watchers
You can create and have multiple watchers going at the same time. For example you might have one that watches for just data changes, another for selection changes, one for background color changes and another for changes in font color.
var backgroundWatch = ClientWatcher.addWatcher ({ watch:{active:false}, domain:{property:'backgrounds'} }); var colorWatch = ClientWatcher.addWatcher ({ watch:{active:false}, domain:{property:'fontColors'} }); var valuesWatch = ClientWatcher.addWatcher ({ watch:{active:false} }); var activeWatch = ClientWatcher.addWatcher ({ watch:{data:false} }); backgroundWatch.watch (function (current,pack,watcher) { // the background colors have changed }); colorWatch.watch (function (current,pack,watcher) { // the font colors have changed }); valuesWatch.watch (function (current,pack,watcher) { // the data has changed }); activeWatch.watch (function (current,pack,watcher) { // one of the active selections has changed });
Stopping polling
You can use watcher.stop() to finish polling (it will stop on completion of the currently active poll) and watcher.restart().watch(callback) to start it up again. Removing a watcher ClientWatcher.remove (watcher), will stop it, then remove it.
Watching parts of sheets – scope
By default the entire active sheet is watched for changes. But you can watch just the active selection, or even some other sheet or range. Here’s a few examples
Watch for data changes in the active selection
var valuesWatch = ClientWatcher.addWatcher ({ domain:{scope:"Active"} });
Watch for data changes in a specific sheet
var valuesWatch = ClientWatcher.addWatcher ({ domain:{sheet:"Sheet2"} });
Watch for data changes in a specific sheet and a specific range
var valuesWatch = ClientWatcher.addWatcher ({ domain:{sheet:"Sheet2",range:"a1:a10",scope:"Range"} });
Watch for data changes in a specific range in the active sheet
var valuesWatch = ClientWatcher.addWatcher ({ domain:{range:"a1:a10",scope:"Range"} }); Chang
Changing watching without stopping polling
You can change what your watching midstream. This example stops watching the sheet but starts watching an active selection. This makes it really easy to implement a radio button to swap between watching a whole sheet and the active range

DomUtils.elem("whole-sheet").addEventListener ('change' , function () { watcher.getWatching().domain.scope = e.target.checked ? "Sheet" : "Active"; watcher.poke(); // this is optional, but it will provoke a refresh of all the polling info. });
Polling frequency
Apps Script does not have a binding capability, so Watcher simulates this by polling the server. The pollingFrequency property is how often the client asks the server (in ms) to look to see what’s changed. If you have multiple watcher, they can each have different polling intervals. You should set this from the default dependinding on how urgently changes need to be reflected in the client.
Fiddler
In A functional approach to fiddling with sheet data I covered how to play around with sheet data in a structured way. You can instruct the watcher to organize your data values like this too if you want. It will make it a lot easier to manipulate and access than the usual values array. By default, a fiddler is created, and you can access it from the callback. So to get your data in structured format, use
current.fiddler.getData();
If your data is not suitable for fiddling, or not required, you can turn that off when you set up your watcher with domain.fiddler = false; I highly recommend you spend some time reading up on A functional approach to fiddling with sheet data and using that to manipulate data returned from the sheet.
Setting up
I have created a shared test add-on to demonstrate what gets returned from the watcher. Play around with that and see how it works. There are a number of modules required, and I use my own style for writing modules that will run both server and client side. If you do something else, you’ll need to tweak the code – which you’ll find on github or here.
Here’s what they are all for
Script | runs on | purpose |
Include | Server | Pulls in server side code to run on the client |
Provoke | Client | Manages conversation with server side |
Utils | Client/Server | Various Utilities |
DomUtils | Client | Dom manipulation stuff – just required for demo |
Fiddler | Client/Server | Manage data for structure access. I’m running this on client, but could be server also if manipulation required before sending |
ClientWatcher | Client | The watcher namespace |
ServerWatcher | Server | The co-operating server namespace |
main.js.html | Client | Just for demo |
addon | Server | Just for demo |
Promises
I use promises throughout these scripts, so if you need to support very old browser, you may need a promise polyfill client side. You can get one by including this in your script.
<script src="//cdnjs.cloudflare.com/ajax/libs/es6-promise/3.2.1/es6-promise.min.js"></script>
Visibility
One problem about polling is that you might be using up bandwidth or quota polling for changes that aren’t happening. To minimize unnecessary polling, ClientWatcher is able to detect whether the document is actually visible (in other words the user is on the browser tab that hosts it). If it’s not visible, it suspends polling until the user switches back to it. So no polling is done unless the user is looking at it.
It uses this library to do that.
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/ifvisible/1.0.6/ifvisible.min.js"></script
You can change to poll all the time with one of the clientwatcher options
pollVisibilityOnly:false
Demo
The demo on github just shows changes in the sidebar. You can try it here. It looks like this
