Hey Mike,
I love the idea of a pop-up debugger like that! That's great. I'm a big fan of making things visible, and I've been thinking for a while that developers will eventually need to be able to debug reactive updates. I hadn't thought of the "log with mouseover" format.
This is indeed an active area of work, but I can tell you exactly how we update the DOM currently. I've been meaning to write it up.
The biggest thing to realize is that even if a template is re-rendered, that doesn't mean all its nodes are replaced. We use the new HTML as a starting point and essentially generate a patch to apply to the DOM, using the IDs of the nodes to inform the diff. (The way to test for sure whether a given element was updated or replaced is to assign the node to a variable before the update and compare it after.) For example, if you take the skeleton app and add an ID to the H1, you'll find the H1's attributes are updated in place. Internally we construct the new DOM in a fragment and copy over the new nodes and differing attributes.
Tom: Your square animation will work if you add an ID attribute to the square!
Mike: What you are observing is the dependency logic for when a template is re-rendered, which is interesting in its own right. As you may know, certain calls like Session.get register a data dependency. When the data changes, it sends an invalidation signal to whatever the "current" context was at the time of the call, basically the current template, which is re-run from scratch, including all its sub-templates. The available techniques to reduce redraw at this level are coarse; basically, move the data access as far down the tree as you can, and wrap it in its own template if you don't want that part of your display to redraw its siblings. (If you want to get fancy, you can create a context boundary without starting a new template by calling Meteor.ui.chunk from a helper.) But with the advent of DOM patching, this kind of care is much less necessary, and there is little reason to create additional templates for this purpose.
Here are some questions and answers.
## When is it important to preserve DOM elements instead of replacing them?
Text input: At first glance, it seems like you could replace a text field if you save and restore 1) the focus and 2) the selection or location of the insertion point. However, when you take into account international text input and assistive input methods, there is more state than this, and the state is not accessible to JavaScript.
For example, to type French diacritical marks on my Mac, I can type option-{E,I,U,`} and then type a vowel. This feature has been around for 20 years and is called "dead keys." After the initial key combo, the OS waits indefinitely to see what letter comes next. (Originally there was no visual feedback, but now you see a little accent with an underline or a colored box while it's waiting. Even more recently, there's an additional UI carried over the iPhone -- you can hold down a vowel to see a little menu of variants; try it!) For Asian language users (e.g. Japanese) multi-key input is a way of life; words are composed into a little pop-up box or an inline autocorrect-like interface. Then there's dictation and other alternative ways of text input. All OSes ship with this stuff, it's just not usually turned on for normal English-speaking users. Browsers are starting to have some events to tell JavaScript when this stuff is happening (compositionstart, compositionend, oninput), but there's way to access the state.
Controls in general: When you think about it, every control has some state. If the user is holding down a button, or navigating a pop-up menu, replacing the element will abort that interaction and be disruptive. Even moving or removing and re-adding the same element is no good.
Animation: What Tom said. :) I hadn't thought through the animation case before, but basically it has to be the same element, new property -- not a new element -- to get the CSS transition.
iframes: There's no way to replace an iframe, or even move it, without completely reloading it.
Widgets, in-DOM state: You may have code (third-party or your own DOM hackery) that manipulates an element and expects it to stick around with the same identity and expando properties.
## Why does Meteor diff DOM nodes, even with structured templates and dependency tracking?
In theory, we could construct a system with perfectly narrow reactive sources and sinks, where a data change maps to a precise DOM mutation. Consider an #if statement in a template whose condition depends on some calculation on the database. Ideally, we would connect a wire from the true/false outcome of the test -- not, say, the contents of an entire database document -- to the location in the DOM where the #if occurs -- not the entire template. When the condition toggles, and only then, we would go in and do a minimal replacement in the DOM. This technique is favored by frameworks like DerbyJS and Asana.
The problem is that requiring the dependencies to be 100% exact is too constraining, and it places a burden on the developer and the framework. One of Meteor's principles is that a template is just a function; it doesn't matter if, internally, it uses stock Handlebars, custom helpers, snippets of HTML, some other template language, or pure JS. We can't necessarily reach inside and find the control structures and where they map to in the DOM (if anywhere). To do that, we'd need full reflection capabilities on the template, the code, or both. This is why Asana started by creating a language and DerbyJS is hard-coded for Handlebars.
Instead, we use template boundaries as the places where we know both 1) how to recalculate the HTML and 2) where to put it in the DOM when we do. We allow both data invalidations and DOM updates to be overly broad, and the DOM diff/patch system provides an escape valve by catching the over-draw.
That said, we aren't afraid to draw inspiration from the "ideal" model when it's practical. For example, Session.equals allows you to depend on a true/false outcome, and we hack Handlebars' #each to call Meteor.ui.listChunk and implement a fully reactive list.
## How does diff/patch work?
When a template is re-rendered, the new DOM tree for that template is held in a fragment. The old DOM tree is in the document. We don't have any more information about the deeper relationship between the old nodes and the new nodes. At first it seems like we could use some rough heuristics to interpret what has changed. Surely if both renderings consist of a single IMG tag, or a DIV followed by a TABLE, then it's the attributes and contents of these tags that have changed, not their presence at all? But consider a template that contains three input fields before the update, and three input fields after. Are they the same input fields, or completely different ones? Perhaps the middle one disappeared and a new field appeared at the end. Payment processing forms often have fields that come and go, for example if the user selects "pay with PayPal" or chooses a foreign country that doesn't have zip codes. If we are too eager in our guesses, the results could be very confusing for the user and developer. The user's focus or entered text could jump from one field to another, for example.
We call this problem of finding the nodes that correspond the "matching problem." You could pose it generally as: A user-specified function is executed and creates a bunch of objects (in this case, nodes). Then we execute the function a second time, and it creates a different set of objects. Which ones are which? That is, which of the objects created the first time are still present, so we don't have to tear them down?
The way we go about it is to look for nodes in the new DOM that seem to match existing nodes in the old DOM, based on their ID attribute. We also look at the "name" attribute, which is traditionally set on INPUT elements (and only INPUT elements, according to the latest HTML spec), and treat it as a locally unique rather than globally unique label. (Developers often use the "class" attribute for non-unique names, but it makes a poor signal for matching. So what if an old and new button both have the class "highlighted btn_group my_button", does this mean they're the same button or not?)
A match indicates that a node remains present across the update, and so we try to keep the node in place. If there was a <div id="foo"> and a <div id="bar"> in the old DOM, somewhere in the template, and there is in the new DOM, too, we try to leave these two nodes in place and work around them, keeping their parents but replacing their siblings and children. I say "try" because this is only possible if the nodes in question haven't in fact moved, been re-ordered, gotten new parents, etc.
The rule for getting an element preserved is: Give it a unique ID, or a unique name within an enclosing unique ID (or name). Don't have duplicate IDs (which sometimes happens by accident since browsers don't complain). Also, the elements you want to preserve can't change order. If you have elements with IDs changing order, they will throw off element preservation -- some of the elements within the immediately enclosing element with an ID will be replaced instead of updated.
Avi encountered this "no changing order" constraint in an app recently, and it bears some explanation. Browsers can't really "move" nodes, even internally; they just remove and re-insert them into the DOM. Pretty much all the problems with element replacement (listed above) are still problems if we move the element in any way -- input state, iframes, animations. Only the last kind of state, state attached to the DOM element from JavaScript, is preserved if we keep the same node and move it. If you are trying re-order iframes, or animate re-ordering, you'll have to do some custom stuff with the element positioning no matter what.
You actually can re-order elements while preserving the DOM nodes, though, using lists.
## How do lists (#each) work?
We hack Handlebars #each to behave in a special way when passed a database cursor.
Each time the body of the #each is run, it's run in a new dependency context, so in a sense it's as if you defined a separate template and invoked it multiple times. When the set of documents changes, the list is never completely re-rendered, only on a per-document basis. Internally, the database cursor is observed for added, removed, moved, and changed callbacks, and each callback results in a fine-grained mutation to the DOM. "Added" renders the body of the #each for the new document and inserts the rendered fragment as a list item. "Removed" removes the rendering of the document from the list. "Changed" causes the list item to be re-rendered in place. "Moved" pulls out the DOM nodes of the list item and re-inserts them, without re-rendering it.
## What's next?
Forms; storing data in templates and preserving it across updates; preserving embeds like Google Maps and D3; callbacks when a template is rendered; access to the template hierarchy at runtime.
The biggest issue is that right now, when a template is re-rendered, we lose all the state associated with the sub-templates; at best, we hold onto some of the DOM nodes. The basic reason for this is another version of the matching problem; if template "foo" calls template "bar" in a bunch of places with different arguments, possibly from inside #ifs and loops, how do we know which invocation is which? Does the developer have to annotate them? Meteor doesn't get to look at the template source code -- it's template language agnostic -- only to watch what it does when run.
Once we have "template invocation matching," we can allow you to stash data locally in a template invocation, instead of in the DOM or in Session with a unique name. We can also have a helper for "make this region immune from reactive updates," to better support embeds, and if an update happens that would normally replace it, the patcher will instead patch around it.
I'll probably put some of this text up on the wiki.
-- David
You received this message because you are subscribed to the Google Groups "meteor-talk" group.