Watching docs for changes

In Watching for server side changes from the client html service, I showed how to watch for server side positional or data changes from the client and be woken up when they happen. That post covered spreadsheets, but you can also do the same thing with Documents. The client side is almost exactly the same, but of course the server side has changed. 

Watching documents is a little more complex, because the data in them is less structured, the object model is a little more complicated and there's more things to watch out for. However the client side is as simple as before. 

Here's an example of how you would use it. 

  • create a watcher. This time we have to change a couple of the defaults to watch a whole document, rather than a whole sheet.
var dataWatch = ClientWatcher.addWatcher ({
    domain: {
      app:"Docs",
      scope:"Doc"
    }
});

  • Use it. It will call you every time there are any changes of data or selections in the active document
   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 these 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 and the cursor
 data the text from the area being watched
 dataSource Information on where the data came from

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. 


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.  


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 some of the capabilities using the demo

Setting up a watcher. 

The default values are set up for sheets. So it needs a couple of changes to watch for Docs
 // default settings for a Watcher request
    var watch = Utils.vanMerge ([{
      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 (this is the selection. in docs it also triggers if cursor changes)
        data: true,                                   // whether to watch for data content changes
        sheets:true                                   // watch for changes in number/names of sheets (only for app.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 sheets or docs                   
        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
      }
    },options || {}]);

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 data, but does not monitor for changes in active selection. 

var watch = ClientWatcher.addWatcher ({
  watch:{active:false},
  domain:{app:'Docs',scope:'Doc'}
});

Note that right now the only property that is watched in documents is the text content, unlike the Spreadsheet watcher which can watch for color changes and all sorts of stuff. I made add this to the document watcher too if enough people ask for it.

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 Docs - scope

By default the entire active document is watched for changes. But you can watch just the active selection or cursor position, a named range or a path (more on that later). Note that the selection and cursor position are different things in Docs (unlike in Sheets where they are the same) - so sometimes the selection will be null, or the cursor will be null to indicate what the user is up to. To avoid  getting two callbacks for each cursor/selection changes this watch is combined and provoked by either.

Watch  for data changes in the entire document

  var valuesWatch = ClientWatcher.addWatcher ({
    domain:{scope:"Doc",app:"Docs"}
  });

Watch  for data changes in the active selection or cursor position

  var valuesWatch = ClientWatcher.addWatcher ({
    domain:{scope:"Active",app:"Docs"}
  });

Watch  for data changes in a specific named range

  var valuesWatch = ClientWatcher.addWatcher ({
    domain:{scope:"Range",range:"MyNamedRange",app:"Docs"}
  });

Watch  for data changes in a path

  var valuesWatch = ClientWatcher.addWatcher ({
    domain:{scope:"Path",range:{path:'00000.00001',partial:true,offset:2,endOffsetInclusive:4},app:"Docs"}
  });

Paths

Docs don't have cell addresses like Sheets has, and a native range can't be passed from client to server. To get over this, Watcher uses a path  to describe where something is in the document. 

They look like this. See how the 'address' of a selection is conveyed through an array of positional paths. The offsets also describe whether the entire element at that path is used, or just a part of it.


Since a range can span multiple elements, you might get something like this. The text spanned by all these elements is returned concatenated, so you may not be interested in the actual elements. If you are, there is some code in the serverwatcher module to map these back to ranges if you need it.


All ranges are described using this method. 

You can use this method to watch a particular area of a document by using the scope:"Path" and passing a single item from the array show. Note at this time, scope:"Path" can only watch one element (or part of an element). I may enhance that later if asked. Note also that there is no unique id that stays with an element in Docs, so the path is dynamically calculated at each poll - in other words if someone inserts a paragraph it may mean that the path changes. If you want to anchor to a particular paragraph, then you'll need to use the named range approach instead.


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. 

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 - sheets
 addon Server Will detect whether its doing sheets or docs and load the appropiate client files
 index.html Client Just for demo - sheets
 mainDocs.js.html Client Just for demo - docs
 indexDocs.html     Client Just for demo - docs
 

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 its 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 and in the console. You can try it here.


For more like this, see  Google Apps Scripts snippets. Why not join our community , follow the blogtwitterG+  .

You want to learn Google Apps Script?

Learning Apps Script, (and transitioning from VBA) are covered comprehensively in my my book, Going Gas - from VBA to Apps script, available All formats are available now from O'Reilly,Amazon and all good bookshops. You can also read a preview on O'Reilly

If you prefer Video style learning I also have two courses available. also published by O'Reilly.
Google Apps Script for Developers and Google Apps Script for Beginners.



Comments