Beyond clones: synchronized DOM nodes

For years, I’ve tried to answer a simple question: How do you show the same data in two places at once, in Mozilla? In particular, DOM nodes. The simple answer is to clone the data you have in one location, and place the clone in another. This is not enough when the data changes in one location; I want it to change in the other, too.

Possibilities include:

  • A split view of the same document, particularly for editing
  • My latest attempt at a scrollable content object model (XBL at work, again)
  • Correcting markup in one section of code, and having it update a dependent section later automatically. Think slide shows, where two slides are nearly identical – and a semantic relationship exists between them. Also think steps of a theorem proof.

An ideal solution might involve a special layout frame for transposing the data, but that’s more work in an area I’m unfamiliar with. A solution I can do (more expensive memory-wise, but at least workable) involves setting DOM mutation listeners. Read the extended entry for more details, and a ZIPped source directory for my own synchronizer – if you don’t mind some technobabble.

In short, there are three parts to keeping a pair of nodes synchronized: managing mutation listeners, detecting changes and reflecting them. The mutation listeners exist for the sole purpose of handling the latter two parts.

Gecko 1.9 actually makes most of this pretty easy. With the UserData interfaces of DOM Level 3 Core, I can define references between nodes in one DOM subtree, and equal nodes in a second DOM subtree. DOM Level 2 Traversal helps find every single node in each tree, so I can create the references. DOM Level 2 Events support means I can detect when a change happens and act on it. DOM Level 2 Range provides a way to watch over whole sets of DOM nodes – providing I define another specialized mutation listener to watch over the contents of the range. DOM 3 Core’s isEqualNode method (and the xpcshell test harness) means I can test the scripted synchronization and ensure that it really did update correctly.
The primary event listener that keeps the two trees synchronized is about 130 lines of JavaScript.

The fun begins when you realize all the little ways a synchronizer can go wrong.

For instance, say your synchronizer is watching Node1-Node2 and Node2-Node3. If you’re not careful, a user could define another relationship, Node1-Node3. The result is that Node1 updates Node2, which updates Node3, which updates Node1 again… an endless loop.

Or, say one of the nodes you’re watching is a document fragment. Bad idea. The moment you tell the DOM to append that document fragment to a node, the children of that document fragment will be appended instead. (That’s how document fragments work, and it’s why I’ve always thought of document fragment nodes as a type of DOM clipboard.) The result is that the DOM removes those nodes from the document fragment. The synchronizer then removes the equivalent nodes from the paired node, and you’ve got data loss.

Another nasty scenario is when one node you’re synchronizing contains the matching node. They can’t possibly be equal under that scenario, and it all falls apart. (If the node pairs you’re watching aren’t equal, or the range pairs aren’t equal, what’s the point of trying to synchronize them?)

Finally, if you attach the same mutation listener to both trees, you’d best make sure that when one node mutates and it causes the second node to mutate, that it doesn’t again cause the first to mutate.

Thus, validating the arguments for synchronization is extremely important. If you want to watch ranges of nodes (as I do), the complexity required for validation is then squared.

It also makes sense that if you want to start synchronization at one point, you’ll eventually want to stop it at another. So providing API for detaching the synchronizer from a node pair or range pair is a good idea.

Ranges can change their positions too – so when you start watching a range, you really want to watch a clone of the range which you own (and never change by yourself).

Fortunately, I’ve covered all of these. (I think.)

DOM Synchronizer, source code

I should probably make a XPI out of this at some point, but I wanted to put this out for feedback as-is. This is a XPCOM component, implemented in JavaScript, for doing the grunt work of synchronization. It has four important methods: attachByNodes(), detachByNodes(), attachByRanges(), and detachByRanges(). You would pass in the pairs of nodes or ranges to watch or to stop watching as the arguments.

However, the DOM Synchronizer must also work – and when it stops working, you need a way to find out. For this, I also included a fifth method, hasInternalError(). (In the case of an internal error, all synchronization stops and the four primary methods would all throw an exception.)

The ZIP file also contains a XPCShell test case which beats up on the DOM Synchronizer a bit. I probably need to write more tests for the validation routines…

This code has been tested and implemented for Firefox 3.0 beta 1 and Gecko 1.9b1 equivalents. Known bugs:

  • ###!!! ASSERTION: Event listener manager hash not empty at shutdown! – this one, as far as I can tell, isn’t my fault directly. It happens after the test finishes running, but my test logs show all my event listeners have been accounted for and removed. I need some investigation of that, and I should probably file a bug.
  • When a page unloads, and it contains a DOM node the synchronizer watches, by definition the node goes away. The synchronizer still keeps a reference to it, though… and I don’t know how it will react, especially when the matching node hasn’t been unloaded yet. I suspect memory leaks at the least, and possibly even crashes.

I’m sure there are a bunch of possibilities for this synchronizer that I haven’t thought of yet, but it’s a start. I’d appreciate it if anyone still reading this could take a look at it…

UPDATE: The assertion failure is gone on trunk. So are a ton of noisy NS_WARNING calls. Yay!!!

4 thoughts on “Beyond clones: synchronized DOM nodes”

  1. When I struggled with this particular issue in a hobby-style-project, I used a datasource with XUL templates.
    It was a huge pain to deal with RDF datasources. (I’ve heard there are alternatives now.) But it always seemed a very aesthetic way of handling things: separation of form and content etc.
    I guess that breaks down when you want the resulting document to be as mutable as the rest of the page, though. 🙂

  2. One question is whether the two renderings will
    a) have the same layout
    b) have the same style but different layout (e.g. because they’ll be reflowed with different available widths)
    c) have different style and layout
    For a), it would be pretty easy to add a new element/frame to Gecko that just “mirrors” the rendering of some other DOM subtree. This would be quite useful for various things, like live preview thumbnails.
    (From Alex: I’d love to see it done – it’d obsolete the need for my synchronizer and, as I pointed out earlier, it wouldn’t eat as much memory. If you want to file a bug for it, please cc me. I’d take a XPI as well.
    There’s the obvious weakness with separate nodes in that CSS can force different renderings, especially with carefully designed selectors.)

  3. > How do you show the same data in two places at once, in Mozilla?
    It’s XForms.
    (From Alex: You’ll have to do better than that. A code snippet would be nice.
    Besides, most Mozilla users don’t have XForms installed by default.)

  4. > It’s XForms.
    > (From Alex: You’ll have to do better than that. A
    > code snippet would be nice.
    This will show two ‘hello’ labels. Here xf:output elements are attached to ‘data’ element in instance document. If you change ‘data’ element by DOM methods then you need refresh model yourself (by calling methods on xf:model via JS) but if you do this by another xforms elements (it’s usual way for XForms) then it will be updated automatically.

    <instanceData xmlns="">
    <xf:output ref="/instanceData/data"/>
    <xf:output ref="/instanceData/data"/>

    > Besides, most Mozilla users don’t have XForms
    > installed by default.)
    That’s major restriction of XForms. Probably XForms is an excellent thing for XUL based applications or HTML applications targeted on intranet.

Comments are closed.