Open trusktr opened 10 months ago
There are some other issues I stumbled on of people wanting to observe the composed tree, but I can't find them right now. It will be nice to link them here.
I haven't added tests for these (slotchange) cases yet
I've added tests to Lume here locally now, and have verified they easily break expectations just as with MutationObserver
. I'll will push this up soon.
My algo relies on features like assignedSlot
in get composedParent() {}
for finding a node's composed parent, which will simply break with closed ShadowRoots. So basically I will be forced to patch DOM APIs anyway and avoid relying on assignedSlot
.
All Lume current elements have mode:open roots, but I can't guarantee that some user of Lume doesn't make a new element with mode:closed. EDIT: Hmm, well I suppose I can patch global attachShadow
in Lume to force them to always be open. Not sure if this has any negative implications for people who want closed roots though.
There's indeed a great need for observing the composed tree. And the MutationObserver
APi is obviously a far cry here.
My initial thoughts on a realdom
-based solution are:
<span slot=""a"><div>
is added in light dom and shadow dom has a corresponding <slot name="a"></slot>
, we can tell the composition and fire the relevant events.Not sure how much of a good idea this is but will give things a try.
I think if we can make something reliable and easy to use, its a good idea!
I'd imagine thise would be built on top of realtime, and could possible be a separate module (import separately only if needed, to avoid globals that have a bunch of unused APIs).
I'm imagining there'd be something separate for use on any element from the outside:
class ComposedChildObserver {
constructor(callback) {
this.callback = callback
}
observe(element) {
// implement with realtime()
}
}
const observer = new ComposedChildObserver((changes) => {
for (const change of changes) {
for (const composed of change.composedChildren) console.log(composed)
for (const uncomposed of change.uncomposedChildren) console.log(composed)
}
})
observer.observe(someElement)
or similar. And maybe then also ComposedParentObserver
that for the given node notifies when its composed parent changes.
And then a mixin like what I have in Lume could be impemented using those:
function CompositionTracker(Base) {
return class extends Base {
composedCallback(composedParent, compositionType) {/*...subclass implements...*/}
uncomposedCallback(uncomposedParent, compositionType) {/*...subclass implements...*/}
childComposedCallback(composedChild, compositionType) {/*...subclass implements...*/}
childUncomposedCallback(uncomposedChild, compositionType) {/*...subclass implements...*/}
// ... use realtime() as needed to call those methods if they are defined ...
}
}
After we get that far, it would be interesting to implement reactive interfaces with Solid, f.e.:
const parent = createParentSignal(someElement)
const parent2 = createParentSignal(anotherElement)
createEffect(() => {
// any time either parent changes, log:
console.log(parent(), parent2())
})
And then after that :D perhaps an object API that creates the instantiates the underlying observation primitives only upon access (downside is it includes all code, so importing this API would have a bigger size):
const proxy = createElementProxy(someElement) // not sure about the `createElementProxy` name, but for sake of example
createEffect(() => {
// any time the parent, children, or rootNode change, log them:
console.log(proxy.parent, proxy.children, proxy.rootNode)
})
createEffect(() => {
// log pointer states (bunching them up in a single effect is probably not what we want, but for sake of example):
console.log(
proxy.pointer.down.x, proxy.pointer.down.y,
proxy.pointer.move.x, proxy.pointer.move.y,
proxy.pointer.up.shiftKey)
})
^ See the whatwg/dom issue I linked.
I've added tests to Lume here locally now, and have verified they easily break expectations just as with
MutationObserver
. I'll will push this up soon.
I've added more manually-operated tests here. I see you implemented the cross-root
feature. Looking forward to checking that out. So it looks like you're getting closer to this:
- use heuristics (following the slotting algorithm) to statically determine the composition and fire corresponding events. F.E.: if
<span slot=""a"><div>
is added in light dom and shadow dom has a corresponding<slot name="a"></slot>
, we can tell the composition and fire the relevant events.
I think this might be a good foundation to start to observe the composed tree synchronously with. What I want to do is run logic synchronously when
and similar for all uncomposed reactions.
I have an initial version of this in my
CompositionTracker
class mixin. It has some limintations: currently it only works with a tree of elements that all extend from the same base class (i.e. it only detects composition with Lume elements, but not composition of a Lume element into any arbitrary non-Lume element because the implementation relies on custom element callbacks which built-in elements don't have, but that's whererealdom
comes in).Also after trying a bunch of things, my code in that area has become a bit messy, and some details that should be in
CompositionTracker
are currently also here inSharedAPI
. I need to clean it up and makeCompositionTracker
full generic, and I'd like for it to detect composition for a custom element regardless if the custom element is composed to a custom element or built-in element.There's a problem with
slotchange
events: we do not get a set of mutation records, we can only detect the final nodes that are slotted, and for example we cannot run observations for nodes that are both removed and added within the same tick:But I believe that with
realdom
we can synchronously track the set of possible mutations that would lead to a slotchange event being fired, and instead of relying onslotchange
we can fire our own handlers at those moments.With
slotchange
events, just as with theMutationObserver
ordering problem,there is also the chance that when a node is unslotted from one slot and slotted to another, the slotted reaction may fire before the unslotted reaction, leaving things in a broken state. Because of this, some edge cases in Lume are for sure broken (I haven't added tests for these cases yet, but when Lume goes mainstream, I really don't want anyone to run into such obscure problems).