A different way to add bindings to DOM nodes – with proof of concept!

Several years ago, before HTML 5 reached its current adolescence, it was in
diapers. There was a sub-specification called Web Forms 2.0, and it included
a
repetition model for blocks of HTML markup
. The idea has long since
been dropped from HTML 5 (it’s probably in a landfill with someone’s diaper),
but it’s remained an inspiration (or maybe an obsession!) for me ever since.

Making it work is quite challenging. I think a repetition model could be
very useful for any web page editor – or for Verbosio, my experimental XML
editor. There’s a few drawbacks, though:

  • I can’t use XBL to pull it off. XBL wants me to insert content as
    children of the bound node, and I think that could just mess
    things up badly for CSS. Inserting the anonymous content as previous siblings makes more sense, and is cleaner, both for DOM and CSS manipulation.
  • I can’t use XTF for it. Well, I could, but the code gets very
    ugly. Unmaintainably ugly.
  • I really do need to hide these inserted elements from the DOM, and yet
    show them in layout. This is one of the issues that prevents a JS-based XBL
    2.0 implementation, or a JS-based XBL 1.0 implementation.

Given all that, I finally hit on a solution: copy the DOM. Give one
DOM tree to the primary JavaScripts, and another DOM tree to the web browser.
Unfortunately, I couldn’t copy it just once. I had to copy it several times,
maintaining private copies of the full DOM tree, one for each new “layer” of
bindings.

If all this is boring and you want me to show you something, here’s a
screenshot:

Yes, that’s a chrome Mochitest, with a DOM Inspector window. DOM-I (and the
browser window) sees the “anonymous content” DOM, but all the tests I ran
against the “scriptable” DOM.

Let me play with this!

Okay.

hg clone http://hg.mozdev.org/verbosio --rev 06a72a32d6ec unstable
cd unstable
python client.py --target=floor13 --mozilla-rev=--latest-beta checkout
python client.py --target=floor13 build
. run_bindings.sh

Source code for this experiment lives at

http://hg.mozdev.org/verbosio/file/06a72a32d6ec/experimental/floor13
.

How does it work? Can this be used for XBL 2? What about my own
properties?

Extended entry, please. For those who don’t wish to read on, well,
Happy New Year!

There’s several pieces to making this work.
First, there’s a one-to-one
hashtable, where the primary columns refer to different DOM layers. There’s the
public “scriptable” DOM, what I call “level 0”, and the “anonymous content” DOM
for starters.

Table 1

“Level 0” DOM “Anonymous Content” DOM
    <html:table>
<markup:repeat min="1" count="2" markup:observes="items=$rowCount">
<html:tr>
<markup:repeat min="1" count="2" markup:observes=".columns=$rowCount">
<html:td>
<markup:children/>
</html:td>
</markup:repeat>
<xul:hbox>
<xul:label value="Columns"/>
<xul:textbox markup:broadcasts=".columns=$value" value="4" size="3" type="number"/>
</xul:hbox>
</html:tr>
</markup:repeat>
<xul:hbox>
<xul:label value="Rows"/>
<xul:textbox markup:broadcasts="items=$value" value="3" size="3" type="number"/>
</xul:hbox>
</html:table>
    <html:table>
<markup:repeat min="1" count="2" markup:observes="items=$rowCount">
<html:tr>
<markup:repeat min="1" count="2" markup:observes=".columns=$rowCount">
<html:td>
<markup:children/>
</html:td>
</markup:repeat>
<xul:hbox>
<xul:label value="Columns"/>
<xul:textbox markup:broadcasts=".columns=$value" value="4" size="3" type="number"/>
</xul:hbox>
</html:tr>
</markup:repeat>
<xul:hbox>
<xul:label value="Rows"/>
<xul:textbox markup:broadcasts="items=$value" value="3" size="3" type="number"/>
</xul:hbox>
</html:table>

Each element represents its own cell in the hash table.

Table 2

“Level 0” DOM “Anonymous Content” DOM
0 <html:table/> <html:table/>
1 <markup:repeat/> <markup:repeat/>
2 <html:tr/> <html:tr/>
3 <markup:repeat/> <markup:repeat/>
4 <html:td/> <html:td/>
5 <markup:children/> <markup:children/>
6 <xul:hbox> <xul:hbox>
7 <xul:label> <xul:label>
8 <xul:textbox> <xul:textbox>
9 <xul:hbox> <xul:hbox>
10 <xul:label> <xul:label>
11 <xul:textbox> <xul:textbox>

Now, in the initial state, each repeat could start with, say, two copies –
that is, two rows, and two columns per row. You might naively think, “Oh, just
go for it all in the anonymous content level”, as I did. It doesn’t look
pretty, though:

Table 3

“Level 0” DOM Owning repeat row “Anonymous Content” DOM
0 <html:table/> <html:table/>
12 <html:tr/> 1 <html:tr/>
26 <html:td/> 13 <html:td/>
27 <markup:children/> 13 <markup:children/>
28 <html:td/> 13 <html:td/>
29 <markup:children/> 13 <markup:children/>
13 <markup:repeat/> 1 <markup:repeat/>
14 <html:td/> 1 <html:td/>
15 <markup:children/> 1 <markup:children/>
16 <xul:hbox> 1 <xul:hbox>
17 <xul:label> 1 <xul:label>
18 <xul:textbox> 1 <xul:textbox>
19 <html:tr/> 1 <html:tr/>
30 <html:td/> 20 <html:td/>
31 <markup:children/> 20 <markup:children/>
32 <html:td/> 20 <html:td/>
33 <markup:children/> 20 <markup:children/>
20 <markup:repeat/> 1 <markup:repeat/>
21 <html:td/> 1 <html:td/>
22 <markup:children/> 1 <markup:children/>
23 <xul:hbox> 1 <xul:hbox>
24 <xul:label> 1 <xul:label>
25 <xul:textbox> 1 <xul:textbox>
1 <markup:repeat/> <markup:repeat/>
2 <html:tr/> <html:tr/>
3 <markup:repeat/> <markup:repeat/>
4 <html:td/> <html:td/>
5 <markup:children/> <markup:children/>
6 <xul:hbox> <xul:hbox>
7 <xul:label> <xul:label>
8 <xul:textbox> <xul:textbox>
9 <xul:hbox> <xul:hbox>
10 <xul:label> <xul:label>
11 <xul:textbox> <xul:textbox>

It’s just as hard to script and keep reliable as it looks. Instead, I’m
going to keep that anonymous level “pristine” – it’s going to remain an exact
copy of the DOM tree with the highest level before it.

So what’s a level? It’s where all the DOM bindings starting at the previous
level would apply. Content, properties, methods, and event handlers. For
instance, let’s copy Table 2 again, but add a “Level 1” for the first round of
<markup:repeat/> elements, and the repeated content from them.

Table 4

“Level 0” DOM “Level 1” DOM “Anonymous Content” DOM
0 <html:table/> <html:table/> <html:table/>
1 <markup:repeat/> <markup:repeat/> <markup:repeat/>
2 <html:tr/> <html:tr/> <html:tr/>
3 <markup:repeat/> <markup:repeat/> <markup:repeat/>
4 <html:td/> <html:td/> <html:td/>
5 <markup:children/> <markup:children/> <markup:children/>
6 <xul:hbox> <xul:hbox> <xul:hbox>
7 <xul:label> <xul:label> <xul:label>
8 <xul:textbox> <xul:textbox> <xul:textbox>
9 <xul:hbox> <xul:hbox> <xul:hbox>
10 <xul:label> <xul:label> <xul:label>
11 <xul:textbox> <xul:textbox> <xul:textbox>

Now we apply the first round of anonymous content to “Level 1”, namely the
outer-most repeat element (row 1).

Table 5

Node “Level 0” DOM Parent binding node “Level 1” DOM “Anonymous Content” DOM
0 <html:table/> <html:table/> <html:table/>
Row 1 from first repeat
12 1 <html:tr/> <html:tr/>
13 1 <markup:repeat/> <markup:repeat/>
14 1 <html:td/> <html:td/>
15 1 <markup:children/> <markup:children/>
16 1 <xul:hbox> <xul:hbox>
17 1 <xul:label> <xul:label>
18 1 <xul:textbox> <xul:textbox>
Row 2 from first repeat
19 1 <html:tr/> <html:tr/>
20 1 <markup:repeat/> <markup:repeat/>
21 1 <html:td/> <html:td/>
22 1 <markup:children/> <markup:children/>
23 1 <xul:hbox> <xul:hbox>
24 1 <xul:label> <xul:label>
25 1 <xul:textbox> <xul:textbox>
End of repeating content
1 <markup:repeat/> <markup:repeat/> <markup:repeat/>
2 <html:tr/> <html:tr/> <html:tr/>
3 <markup:repeat/> <markup:repeat/> <markup:repeat/>
4 <html:td/> <html:td/> <html:td/>
5 <markup:children/> <markup:children/> <markup:children/>
6 <xul:hbox> <xul:hbox> <xul:hbox>
7 <xul:label> <xul:label> <xul:label>
8 <xul:textbox> <xul:textbox> <xul:textbox>
9 <xul:hbox> <xul:hbox> <xul:hbox>
10 <xul:label> <xul:label> <xul:label>
11 <xul:textbox> <xul:textbox> <xul:textbox>

If you look at nodes 13 and 20, each of them have their own
<markup:repeat/> elements. They need bindings too.

The solution is to clone “Level 1” into a new “Level 2″…

Table 6

Node “Level 0” DOM Parent binding node “Level 1” DOM “Level 2” DOM “Anonymous Content” DOM
0 <html:table/> <html:table/> <html:table/> <html:table/>
Row 1 from first repeat
12 1 <html:tr/> <html:tr/> <html:tr/>
13 1 <markup:repeat/> <markup:repeat/> <markup:repeat/>
14 1 <html:td/> <html:td/> <html:td/>
15 1 <markup:children/> <markup:children/> <markup:children/>
16 1 <xul:hbox> <xul:hbox> <xul:hbox>
17 1 <xul:label> <xul:label> <xul:label>
18 1 <xul:textbox> <xul:textbox> <xul:textbox>
Row 2 from first repeat
19 1 <html:tr/> <html:tr/> <html:tr/>
20 1 <markup:repeat/> <markup:repeat/> <markup:repeat/>
21 1 <html:td/> <html:td/> <html:td/>
22 1 <markup:children/> <markup:children/> <markup:children/>
23 1 <xul:hbox> <xul:hbox> <xul:hbox>
24 1 <xul:label> <xul:label> <xul:label>
25 1 <xul:textbox> <xul:textbox> <xul:textbox>
End of repeating content
1 <markup:repeat/> <markup:repeat/> <markup:repeat/> <markup:repeat/>
2 <html:tr/> <html:tr/> <html:tr/> <html:tr/>
3 <markup:repeat/> <markup:repeat/> <markup:repeat/> <markup:repeat/>
4 <html:td/> <html:td/> <html:td/> <html:td/>
5 <markup:children/> <markup:children/> <markup:children/> <markup:children/>
6 <xul:hbox> <xul:hbox> <xul:hbox> <xul:hbox>
7 <xul:label> <xul:label> <xul:label> <xul:label>
8 <xul:textbox> <xul:textbox> <xul:textbox> <xul:textbox>
9 <xul:hbox> <xul:hbox> <xul:hbox> <xul:hbox>
10 <xul:label> <xul:label> <xul:label> <xul:label>
11 <xul:textbox> <xul:textbox> <xul:textbox> <xul:textbox>

… and apply the bindings (new content, etc.) to Level 2, for new (thus far, unbounded) nodes.

Table 7

Node “Level 0” DOM Parent binding node “Level 1” DOM “Level 2” DOM “Anonymous Content” DOM
0 <html:table/> <html:table/> <html:table/> <html:table/>
Row 1 from first repeat
12 1 <html:tr/> <html:tr/> <html:tr/>
“Sub-row” 1 from row 1’s first repeat
26 13 <html:td/> <html:td/>
27 13 <markup:children/> <markup:children/>
“Sub-row” 2 from row 1’s first repeat
28 13 <html:td/> <html:td/>
29 13 <markup:children/> <markup:children/>
End of “sub-rows” from row 1’s first repeat
13 1 <markup:repeat/> <markup:repeat/> <markup:repeat/>
14 1 <html:td/> <html:td/> <html:td/>
15 1 <markup:children/> <markup:children/> <markup:children/>
16 1 <xul:hbox> <xul:hbox> <xul:hbox>
17 1 <xul:label> <xul:label> <xul:label>
18 1 <xul:textbox> <xul:textbox> <xul:textbox>
Row 2 from first repeat
19 1 <html:tr/> <html:tr/> <html:tr/>
“Sub-row” 1 from row 1’s second repeat
30 20 <html:td/> <html:td/>
31 20 <markup:children/> <markup:children/>
“Sub-row” 2 from row 1’s second repeat
32 20 <html:td/> <html:td/>
33 20 <markup:children/> <markup:children/>
End of “sub-rows” from row 1’s second repeat
20 1 <markup:repeat/> <markup:repeat/> <markup:repeat/>
21 1 <html:td/> <html:td/> <html:td/>
22 1 <markup:children/> <markup:children/> <markup:children/>
23 1 <xul:hbox> <xul:hbox> <xul:hbox>
24 1 <xul:label> <xul:label> <xul:label>
25 1 <xul:textbox> <xul:textbox> <xul:textbox>
End of repeating content
1 <markup:repeat/> <markup:repeat/> <markup:repeat/> <markup:repeat/>
2 <html:tr/> <html:tr/> <html:tr/> <html:tr/>
3 <markup:repeat/> <markup:repeat/> <markup:repeat/> <markup:repeat/>
4 <html:td/> <html:td/> <html:td/> <html:td/>
5 <markup:children/> <markup:children/> <markup:children/> <markup:children/>
6 <xul:hbox> <xul:hbox> <xul:hbox> <xul:hbox>
7 <xul:label> <xul:label> <xul:label> <xul:label>
8 <xul:textbox> <xul:textbox> <xul:textbox> <xul:textbox>
9 <xul:hbox> <xul:hbox> <xul:hbox> <xul:hbox>
10 <xul:label> <xul:label> <xul:label> <xul:label>
11 <xul:textbox> <xul:textbox> <xul:textbox> <xul:textbox>

Now, if I move the Parent Binding column to the left, and use display: none on the <markup:repeat/>
elements’ descendants, you can see what I have in mind:

Table 8

Node Parent binding node “Level 0” DOM “Level 1” DOM “Level 2” DOM “Anonymous Content” DOM
0 <html:table/> <html:table/> <html:table/> <html:table/>
Row 1 from first repeat
12 1 <html:tr/> <html:tr/> <html:tr/>
“Sub-row” 1 from row 1’s first repeat
26 13 <html:td/> <html:td/>
27 13 <markup:children/> <markup:children/>
“Sub-row” 2 from row 1’s first repeat
28 13 <html:td/> <html:td/>
29 13 <markup:children/> <markup:children/>
End of “sub-rows” from row 1’s first repeat
13 1 <markup:repeat/> <markup:repeat/> <markup:repeat/>
Row 2 from first repeat
19 1 <html:tr/> <html:tr/> <html:tr/>
“Sub-row” 1 from row 1’s second repeat
30 20 <html:td/> <html:td/>
31 20 <markup:children/> <markup:children/>
“Sub-row” 2 from row 1’s second repeat
32 20 <html:td/> <html:td/>
33 20 <markup:children/> <markup:children/>
End of “sub-rows” from row 1’s second repeat
20 1 <markup:repeat/> <markup:repeat/> <markup:repeat/>
End of repeating content
1 <markup:repeat/> <markup:repeat/> <markup:repeat/> <markup:repeat/>
9 <xul:hbox> <xul:hbox> <xul:hbox> <xul:hbox>
10 <xul:label> <xul:label> <xul:label> <xul:label>
11 <xul:textbox> <xul:textbox> <xul:textbox> <xul:textbox>

If you read it either diagonally from the white-space cells, OR straight down
either of the last two columns, it looks like this:

<html:table>
<html:tr>
<html:td>
<markup:children/>
</html:td>
<html:td>
<markup:children/>
</html:td>
<markup:repeat>...</markup:repeat>
</html:tr>
<html:tr>
<html:td>
<markup:children/>
</html:td>
<html:td>
<markup:children/>
</html:td>
<markup:repeat>...</markup:repeat>
</html:tr>
<markup:repeat>...</markup:repeat>
<xul:hbox>
<xul:label value="Rows"/>
<xul:textbox markup:broadcasts="items=$value" value="3" size="3" type="number"/>
</xul:hbox>
</html:table>

So that’s basically what the “anonymous content” DOM looks like. The scriptable, “level 0” DOM?

<html:table>
<markup:repeat min="1" count="2" markup:observes="items=$rowCount">
<!-- ... -->
</markup:repeat>
<xul:hbox>
<xul:label value="Rows"/>
<xul:textbox markup:broadcasts="items=$value" value="3" size="3" type="number"/>
</xul:hbox>
</html:table>

In this model, I could say:

var cell = table.repeats
.getRangeForRow(0)
.getChildAt(0)  /* first TR element */
.repeats
.getRangeForRow(0)
.getChildAt(0); /* first TD element */

OK, the binding API is a bit obscure. But I can write some code to do further abstractions:

function bindRow(owner) {
owner.getBindingCell = function(index) {
return this.getRangeForRow(index).getChildAt(0);
}
// I'm skipping namespace URI specific usages for simplicity.
owner.repeats = owner.getElementsByTagName("markup:repeat")[0];
};
table.getBindingRow = function(index) {
var rv = this.repeats.getRangeForRow(index).getChildAt(0);
bindRow(rv);
return rv;
}
var cell00 = table.getBindingRow(0).getBindingCell(0);

Now, we’re in business!

Other potential uses

Basically, this approach solves the shadow DOM problem, but by brute force.
You have to be careful which DOM you hand off to which user (the layout engine
gets one DOM, and the page scripts get another).

Could you implement XBL 1.0 in this way? I believe so. What about XBL 2.0? Probably.
XTF? Well, you likely won’t get everything XTF offers. I’m thinking about using
a scheme like this to augment my XTF code, at least until XBL 2.0 arrives.

Is it the right way to do things? Probably not. Just cloning all
the DOM nodes and maintaining private copies will likely eat up a lot of memory.
As you add more and more layers of bindings, the memory efficiency really drops.
I’d recommend using this approach for small-scale stuff. For something large,
like a full-blown web page… try to only bind small fragments of the page.

Here’s what I would suggest: use this approach as a test harness platform,
for real, native-code bindings support. If you want to see XBL 2.0 implemented in
Mozilla, multi-layered DOM binding trees like this can help develop what it should look
like. Then a XBL 2.0 developer can come along and build their native-layer implementation
to match the test harness implementation.

What’s next? Well, I have to rewrite the bindings-attachment algorithm, where
properties and methods are concerned. I’m thinking about using Mozilla Firefox 4’s
JavaScript proxy support
to implement “aggregation by proxy”.

What does this mean? Let’s say you’ve got a DOM node, and you want to apply
a JS object’s methods and properties to it. That’s easy, using __defineSetter__
or foo.method = obj.method. But when you later want to replace the
JS-provided properties and methods with another JS object’s properties
and methods – much like changing a XBL binding – __defineSetter__ can be really
cumbersome to use.

Aggregation by proxy, though, means you could daisy-chain several JS objects
in front of the original DOM node, in terms of method and property priorities.
If you need to replace a binding, you swap out one object in the daisy-chain.
No __defineSetter__ mess, though pulling it off will require a bit of custom
JavaScript code. I’d love to see someone else write it up for me – I see it as
a trivial extension of the forwarding proxy examples.

So, that’s it! This is very much a work in progress. I love having
“proof-of-concept” demos, though… they’re probably my best tool for explaining
some of my deeper ideas. Have fun, and I’ll try to cook something better up
soon.

P.S. Since you made it this far… Happy New Year, indeed!

2 thoughts on “A different way to add bindings to DOM nodes – with proof of concept!”

  1. Yes, repetition model is a missing spare part from WebForms2.
    I had to rewrite it in Javascript, because it’s really too useful for my systems.
    But here are the main diffiulties if you want to do so : variable attributes (for id, names,…) , resetability (restore the setting of the loaded html), events an properties duplication (most of the toolkits i’ve seen doestn’t do it properly…)
    (From Alex: Can you show me where you implemented the repetition model yourself? I should note I use it strictly in an editing-content context.)

Comments are closed.