Proposal: Live fragments


#1

Document fragments are great for templates, because you can just clone them and append them. They make great generic containers for reusable nodes that way. They also make for convenient batch append operations (although the perf argument isn’t as much as it used to be).

Now, while they make for great management of nodes out of the main tree, they don’t make for easy management within the tree - notably, they just empty themselves.

Something I’d like to see is maybe some sort of live fragment support natively. Here’s a few examples of where people have already implemented this in library code:

  • React, Mithril, and friends. It’s very common for more advanced virtual DOM-based libraries and frameworks to support fragments, since they already more or less abstract over the DOM to begin with, and since all they’re doing is patching, they don’t need to worry about much beyond just iterating a bunch of nodes.
  • Ember/Glimmer. They have to know how to add, track, and remove slices in user components, since they support components with multiple children.
  • Angular. They use a virtual DOM-like system under the hood to implement a similar thing themselves.

However, without the assistance of a framework, it gets almost prohibitive to manage subtrees in any significant capacity, and also, libraries have to choose to either wrap one whole element, or return one whole element. This affects CSS layout, and is consequently not that great of a thing to work with.

Live fragment support should be able to cover the common case of just wanting to render a collection of elements without resorting to creating a new element, and that’s pretty much the crux of what I’m proposing.

As for a concrete example, consider a list of dt + dd pairs within a definition list. The HTML spec mandates that definition lists only contain such pairs as direct children. However, if you could use live fragments, you could not only generate them from a list; you could isolate those fragments into a separate component without having to roll your own framework or heavy library to do it.

Also, as a concrete example of how hard it is to work around this kind of scenario, consider the fact that relatively few frameworks implement support for such fragments without having a very significant layer of indirection (virtual DOM, Glimmer’s VM, Closure templates via incremental-dom) over the DOM that basically shifts it to a whole different paradigm. Also, Elm itself does not support nested fragments, Etch implicitly flattens nested arrays and requires that components render a node, and the original virtual-dom library has no support for fragments, either.

So, I feel we should instead have a built-in browser primitive, basically a node proxy, that allows live fragments to be created in a way that elements can still deal with just the unit they expect, and CSS can work as if it’s not even there, but something that browsers could actually use to better optimize the case of groups of contiguous nodes manipulated via proxy rather than directly on the instance.


#2

So, thoughts?

Oh, and to clarify, what I mean by “isolating those fragments into a separate component without having to roll your own framework” is that you could do something like this:**

// Define a component
class ChildComponent {
    constructor() {
        this._visible = true
        this._internal = frag([
            n("li", t("child 3")),
            n("li", t("child 4")),
        ])
        this.body = frag(this._internal)
    }

    update(attrs) {
        if (!!attrs.show === this.visible) return
        if (this._visible) this.body.removeChild(this._internal)
        else this.body.appendChild(this._internal)
        this._visible = !this._visible
    }
}

class ParentComponent {
    constructor(attrs) {
        this._child = new ChildComponent()
        this.body = n("ul", [
            n("li", t("child 1")),
            n("li", t("child 2")),
            this._child.body,
        ])
        // ...
    }

    update(attrs) {
        // ...
        this._child.update({show: attrs.showChild})
    }
}

* Note: the following helpers are used above, to simplify the examples.

function t(str) { return document.createTextNode(str) }

function append(elem, child) {
    if (Array.isArray(child)) child.forEach(c => append(elem, e))
    else if (child != null) this.appendChild(child)
    return elem
}

function frag(children) { return append(new LiveFragment(), children) }

function n(tag, attrs, children) {
    const elem = document.createElement(tag)
    if (children == null && attrs != null &&
            (typeof attrs !== "object" || Array.isArray(attrs)) {
        return append(elem, attrs)
    } else {
        for (const key of Object.keys(attrs || {})) elem.setAttribute(key, attrs[key])
        return append(elem, children)
    }
}

** For what it’s worth, you could already define the parent component today - you just couldn’t define the child component. Also, I know it’s a contrived example - you could factor the child component back into the parent pretty easily, even without using live fragments.


[Proposal] new <ignore> element HTML