In Pseudo binding in HTML service I showed a way of looking for changes on the server from client side htmlservice apps. I’ve taken that a bit further and created a very straightforward structure to build in client-side to react to changes in data (or indeed any property) as well as changes in selection, active sheet and so on.

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


For more like this see Google Apps Scripts Snippets
Why not join our community , follow the blog or follow me on Twitter