Instead of patching the entire set of DOM interfaces, this implementation only paches a single custom element. It provides the bare minimum DOM encapsulation for the element it is applied to, allowing for seamless integration with other libraries and frameworks.
All you have to do is extend the mixin with an optional base class and then implement a childNodes
getter / setter which does the necessary changes to your element based on the complete child list. This keeps the implementation dumb and allows you to figure out how you will distribute / diff / whatever using the updated child list.
When using this childNodes
is an array of real DOM nodes.
import ShadowNode from './shadow-node';
class CustomElement extends ShadowNode(HTMLElement) {
get childNodes () {
return this._childNodes;
}
set childNodes (childNodes) {
this._childNodes = childNodes;
doSomethingWith(childNodes);
}
}
customElements.define('custom-element', CustomElement);
With SkateJS:
/* @jsx h */
import { Component, h } from 'skatejs';
import ShadowNode from './shadow-node';
class CustomElement extends ShadowNode(Component) {
static props = {
childNodes: null
}
renderCallback ({ childNodes }) {
return <span>{childNodes.map(n => n.textContent).join('')}</span>;
}
}
customElements.define('custom-element', CustomElement);
This is necessary because if you do something like the following in React:
<div>
<custom-element>
<light-dom />
</custom-element>
</div>
If <custom-element />
has it's own template and distributes <light-dom />
somewhere in its render tree, or removes it altogether, then React (and all other vDOM libs) will complain on subsequent renders because <light-dom />
was moved and the current state doesn't match the previous state.
What this mixin does, is make it look like <light-dom />
never moved. The exposed shadowRoot
property (or return value from attachShadow()
) is a proxy element that renders directly to the host node. Child accessors and mutators work with a fake childlist that maintains the original appearance, thus tricking anything working with your custom element.
This doesn't patch anything globally; it's isolated specifically to the elements that inherit the behaviour. This means that you retain native behaviour and performance on nodes that don't use it.
- Implement declarative
<slot />
for auto distribution (this complicates SSR because you have to convey default slot content and slot state, if emulating native). Maybe the best way here would be to add a declarative way of slotting, that doesn't emulate all of the native behaviour. The question then is, how does default content work, because it is a common use case for dynamic lists / tables. - Implement
:host
selector. - See if this enables full server-side rendering support (will probaby have to add a flag so no initial distribution occurs).
- Find easier way to do manual distribution.
- Consider using
childrenChangedCallback()
instead of a childNodes setter (setter works better for Skate / functional prop-based stuff). - Use proxy instead of actual element for
shadowRoot
. - Fully implement
outerHTML
setter (doesn't patch the outer node, only inner content). - Peformance test (currently using functional stuff:
concat
etc, rather than mutators likepush
). - Try and find way to undo re-parenting on light node if it's moved by the ancestor tree to somewhere outside of the custom element (possibly unlikely use-case??).