webqit / realdom

Low-level realtime DOM APIs
MIT License
7 stars 0 forks source link

observing the composed tree synchronously #3

Open trusktr opened 10 months ago

trusktr commented 10 months ago

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 where realdom 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 in SharedAPI. I need to clean it up and make CompositionTracker 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 on slotchange we can fire our own handlers at those moments.

With slotchange events, just as with the MutationObserver 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).

trusktr commented 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.

trusktr commented 10 months ago

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.

trusktr commented 10 months ago

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.

ox-harris commented 10 months ago

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:

Not sure how much of a good idea this is but will give things a try.

trusktr commented 10 months ago

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 ...
  }
}
trusktr commented 10 months ago

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)
})
trusktr commented 5 months ago

^ 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.