Monitoring HTML Elements For Change
Tags: javascript, html, webdev
Published: 2022-01-05 11:32:38

While trying to build a simple [clipboard copy button](https://dev.to/thumbone/copy-to-clipboard-first-cut-1j5j), I found the need to [in-line styles](https://dev.to/thumbone/in-lining-styles-client-side-9oi) if they are to survive emailing, and that in-lining could, for very large elements be [rather costly](https://dev.to/thumbone/html-style-in-lining-performance-and-what-to-about-it-4a55) (in time), of which was born a need to [let UI events happen](https://dev.to/thumbone/deferring-to-the-user-interface-3fi9) while doing the job. But because my website is interactive, the element being copied might change while the styles are being in-lined!

In the previous post I introduced the code base to which I begin to refer in these notes. That digression was needed to make sense of the ongoing notes as they refer in greater detail to actual implementation details.

## What if an element changes while we're in-lining?

A couple of things are worth noting here:

1. If I click the copy button, I want a copy of what we see on the browser now, after the change we made. But if the style in-liner is running when I click copy button, some most surprising things can happen, because Copy With Style is nice enough to `await defer_to_UI()` at intervals I've managed to change the element while it's running. Turns out that if I time this right I can get a copy that is half how the element was, and half how it is. A kind of chimera element.
![Chimera](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hdweq9bwwoxxvmswitsl.jpg)
   &nbsp;
   To help visualise this, in a standard use-case of mine I might be presenting a whole pile of tables (they'd be leaderboards in games in case it's important, lists of players and scores), and I might want to highlight certain players, or add or remove columns from these boards and the site has plenty of options to do just that. If I make such a change within 40 odd seconds of the page being presented (during which the initial render of a very large number of such leaderboards is having their styles in-lined ready to arm a copy button) and then click the copy button, I get this chimera of a copy. The first half before the change and the last half after depending on exactly when I change options, or how many times even.
   &nbsp;
   So we need to watch the element for changes, and if it changes, we have to start style in-lining process a new.

2. If we're making changes though and every change we make kick fires a new in-lining of styles, we could see an explosion of in-lining jobs on the event queue. And so we need not just to start style in-lining again we also need some way of telling any running style in-liner to finish, quit, stop, bail.
![Proceed no further](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mati5ui08dqp8mifw704.jpg)

### Watching an element for changes

A [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) is handy for this. We can create one with a handler, that is run every time an element changes. That handler can check to see if a style in-liner is running, and if so ask it to stop, and wait until it has, before starting a new style in-liner. 

In the implementation of `Copy_With_Style`,  `#observe_element` instantiates this MutationObserver and associates it with the element (`this.element`). The observer is stored as `#observer` and `#mutation_handler` is the handler called when `#observer` notices that `element` changed.

A little synchronisation is needed here. If scheduling the preparation for copy (i.e. in-lining of element styles) then we don't want to start watching for changes until the style in-liner has started (there will be a lot of them, changes, that is, to the element, while the page is being rendered). 

To wit, even if we're not scheduling a style in-lining when the DOM is ready, we do not want to start observing the element until the DOM is ready (to avoid a flood change observations). So two schedulers are implemented `#schedule_preparation` which schedules the style in-liner to start when the DOM is ready and `#schedule_observation` which schedules an observer to start when the DOM is ready. These are started by the `constructor` based on the configured `triggers` in Copy With Style.

### Communicating and executing a bail and restart

All the communications required (for one event-triggered function to request a waiting function to bail, and then waiting for it to do so, before starting it anew) takes place with properties of the `Copy_With_Style` instance. 

Specifically it uses:

- `#is_being_prepared` which is set to true when the in-liner (`prepare_copy`) starts.
- `#is_prepared ` which is set to true when it's completed successfully and `this.HTML` and `this.text` are set and ready to copied to the clipboard.
- `#bail` which is set by the observer when `this.element` changes and the style in-liner is running (`#is_being_prepared` is true), to ask it to drop what it's doing and bail.
- `#bailed` is set by the style in-liner to let the observer know it's done that (which it was requested to do).

Remember that JavaScript is (essentially) single threaded and that this requires a to and fro dance with `await defer_to_UI()` which takes on a broader meaning now, and could rightly be thought of, or called, `defer_to_others()` as we use it for one function to communicate with another function.

The observer when running must set `#bail` then `await defer_to_UI()` to re-queue itself and give the style in-liner a chance to run and and it in turn checks `#bail` whenever it continues after an `await defer_to_UI()`. If it sees `#bail` set, it bails, setting `#bailed` first then returns (and thus comes off the queue and is no longer awaiting, has stopped processing elements). Eventually the observer's turn comes up again and it can check if `#bailed` is set. If not of course it can `await defer_to_UI()` again as often as needed until it is. In practice I have never seen it not honoured immediately - when the observer defers to UI it goes to the end of the queue and by definition a callback to the awaiting style in-liner is already on the queue ahead of it. 

The only way that the observer might need a second check of `#bailed` is if the in-liner fails to check `#bail` and act on it when it is called back after it's deferral to UI. 

Currently that only issues a warning to the console if it's encountered, but I've not seen it happen yet and if it did happen it suggests a bug.