-
Notifications
You must be signed in to change notification settings - Fork 308
Make it possible to observe connected-ness of a node #533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
It wasn't clear from the original thread what the use cases were. @pemrouze just said it would be nice, then people started discussing how. We never even talked about why they couldn't use custom elements. I agree that if we had a need for this ability extending MutationObserver would be the way forward but I'd like some more justification in the form of concrete use cases that are hard to accomplish with custom elements. |
Agreed. It's always good to have a list of concrete use cases before adding new API. |
IIRC this was discussed around the time when MutationObserver was designed and the idea was that since one can add childList observer to the document and then just check document.contains(node), that should in principle be enough (and the things to observe was kept in minimum). |
This would be useful for polyfilling custom elements, but obviously that's not a good use case for why custom elements aren't enough. If I can page in my requests around MutationObservers one had to do with observing access shadow boundaries so we don't have to patch attachShadow, which might be relevant here, and have some way of unobserving nodes outside of disconnect(), say when the nodes are disconnected. |
|
Wouldn't a childList observer in document take care of those use cases, at least in common cases? |
It's unfortunately notoriously difficult to do with MutationObserver, and pretty much guaranteed to be buggy (due to it being batched) and inefficient (having to watch every change on the entire document). |
polyfills for custom elements already work and are either based on MutationObserver or DOMNodeInserted/DOMNodeRemoved with a lookup for registered elements in the list of changed nodes. Using polyfills is a weak reason, specially because anythig new needs to be polyfilled too. However I do have a use case in hyperHTML too but I believe React and many other frameworks would love this feature as well. CustomElements mechanism to understand their life-cycle are excellent but not everyone writes Custom Elements based sites and knowing a generic form, input, list element, grid card, has been added/removed would be ace. in hyperHTML I do have control of each vanilla DOM component but I need to do this "dance" per each MutationObserver notification: function dispatchTarget(node, isConnected, type, e) {
if (components.has(node)) {
node.dispatchEvent(e || (e = new Event(type)));
}
// this is where it gets a bit ugly
else {
for (var
nodes = node.children,
i = 0, length = nodes.length;
i < length; i++
) {
e = dispatchTarget(nodes[i], isConnected, type, e);
}
}
return e;
} The dispatching after recursive search is done per each |
@domenic any more thoughts? |
Here's a concrete use case. A editor library wants to know when the editor lost the focus. Because |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as resolved.
This comment was marked as resolved.
Yes, this is much needed indeed. I have a concrete use case for progressive enhancement that is much inspired by the Custom Attributes proposal that was discussed a while back and I've brought into a user-land library. Custom Attributes are sort of like For example: // Listens for PointerEvents and paints Material Design Ripples on the host element
class Ripple {
constructor (hostElement) {...}
connectedCallback () {...}
disconnectedCallback () {...}
// ...
}
customAttributes.define("*ripple", Ripple); <!-- Custom Attributes are element-agnostic -->
<button *ripple>Click me</button>
<!-- They can also receive semi-colon separated key-value pairs as values, just like the style attribute -->
<my-element *ripple="center: true"></my-element> Now, while I would like to see Custom Attributes/mixins/behaviors standardized since To solve this today, using MutationObservers just doesn't cut it when using Shadow DOM. There may be many, changing, roots going up the tree from the node, or there may be none at all! You can observe child lists of all roots and hosts from the node and up the tree, but you have to do a lot of work to track changes to the parent chain and update the observers. But if the node has no parent by the time the MutationObserver needs to be configured, you will never know which root to observe. The "best" way to do this right now (as I see it) is monkey patching the |
I would like to revamp this conversation. It seems that we have more use-cases that can help us to come to an agreement about this feature request. Maybe adding it to the agenda for the F2F meeting. |
Yeah, I think we should just add this. Here's a concrete proposal: Add boolean Add two mutation record types
|
Sounds great. Wouldn't it be sufficient with a simple boolean |
Sorry, I wasn't thinking through. We could just add new mutation record types like |
@domenic The main use case I imagine is with builtins, so we can get connected/disconnected behavior that behaves the same as the callbacks for Custom Elements do. (I didn't see this thread before I wrote WICG/webcomponents#785) It'd be useful in some of the other issues I've had troubles with too.
@rniwa Another use case is simply: someone else made a web app, now we want to manipulate it without necessarily touching their source code (maybe we don't have access to the source, maybe we are writing a browser plugin, etc). There's still the issue of reaching into closed shadow trees though (it still requires patching attachShadow). In a sense the purpose of this feature is more similar to the purpose of jQuery: manipulate existing DOM elements with it. This is in contrast to the act of designing the behaviors of our own DOM elements (custom elements), instead we can use this to manipulate existing DOM elements (builtin or custom). 👍 to this. |
Just want to add my use case: <template>
<p>My Custom Element with a menu</p>
<button onclick="toggleMenu">Toggle</button>
<x-menu>...</x-menu>
</template> That template gets used in a Custom Element where I also call a utility function that takes the custom element as its one param, searches all its children looking for any I'd like this utility function to also remove the listeners when the custom element is disconnected. If the utility class had a way to know when the custom element was disconnected that would be possible. Right now I have to return the collection of event registrations to the custom element and it runs through that list in its disconnectedCallback. |
Not sure if it has been suggested, but one way to make this plus a few more use cases easier would be to add an option to monitor parent changes. It's simpler than the original problem, and in some ways more general. |
Use caseI guess the main use case would be removing subscriptions. In my case, I have a (library based) "component" that saves reference to a constructed element, the component must support callbacks for engaging scripts when the element is connected and pause/suspend those scripts when it's disconnected. In reality, it's a bit more complex since an element may not be disconnected while actually not being in the document. So it should take into account connection/disconnection from a document (either parents or the element itself is connected/disconnected). For accurate work, when #1255 is implemented, the document connection events should fire or they should be accounted by a developer. Implementation ProposalI see it as a separate type NodeLifeEntryCase =
| "connected"
| "disconnected"
// | "parentChanged" // or `reparented`.
// | "parentConnected"
// | "parentDisconnected"
// | "documentConnected"
// | "documentDisconnected"
interface NodeLifeEntry {
target: Node
case: NodeLifeEntryCase
}
class NodeLifeObserver {
constructor(callback: (entry: NodeLifeEntry[], observer: NodeLifeObserver) => void) { }
observe(node: Node): void
unobserve(node: Node): void
disconnect(): void
} The commented out entry cases are debatable, but for simplicity I agree we can start from just Alternative Interface IMHO, this is more restrictive one, it might be difficult to add new connection types if needed, though if we all agree on it - I don't mind. interface NodeConnectionEntry {
target: Node
connected: boolean
}
class NodeConnectionObserver {
constructor(callback: (entry: NodeConnectionEntry[], observer: NodeConnectionObserver) => void) { }
observe(node: Node): void
unobserve(node: Node): void
disconnect(): void
} Why not This is a potential extension for proposal above, but is NOT required. Additionally I need the moment when is reflowed...Additionally I need the moment when is reflowed since connection/disconnection may change the size, but you need to use this recalculated value. So I expect that e.g. With this I come up with the following code that extends type ElementLifeEntryCase =
| NodeLifeEntryCase
| "reflow"
interface ElementLifeEntry {
target: Element
case: ElementLifeEntryCase
}
class ElementLifeObserver {
constructor(callback: (entry: ElementLifeEntry[], observer: ElementLifeObserver) => void) { }
observe(element: Element): void
unobserve(element: Element): void
disconnect(): void
} Currently, the moment when a element is reflowed can be accurately caught with this snippet. Obviously, I would like to have a more general approach to receiving this event. export function onElementPainted(target: Element, callback: () => void) {
new ResizeObserver((_, observer) => {
callback()
observer.unobserve(target)
}).observe(target)
} |
@FrameMuse unrelated to the original discussion, but it is actually impossible to detect when an element is finally painted:
That's not good enough because if some other resize observer callback changes the size (directly or indirectly) of the element, it is possible that more resize callbacks will subsequently be queued and fired. The only sure thing we can do is keep the resize observer enabled the whole life of the element, instead of unobserving it right away, otherwise some reflows may be missed. But note that reflow is not the same as paint, and it is impossible to run logic after paint. The "paint" doesn't happen until after all resize observer callbacks, and it is impossible to register a callback that will run after all See these issues for more on the impossible timing, problems, and solution ideas:
|
What's the need for Also, from the perspective of designing something simple enough for everyone to get on board with, maybe having all of that in a single proposal is too much. Could you trim down your proposal to only this,
without the other hooks, and without |
I want to point out that having an API for synchronous element connectedness (from the outside, not limited to internal custom element methods) will help solve brain-twisting problems like this one: |
Just to clarify the OP:
Is this describing a synchronous API (not an async API like MutationObserver which runs callbacks in a microtask)? If so, that's what I want. A synchronous API. (We already have MutationOberver for async). An idea could be to expand myMutationObserver.observe(el, {
// ...other options as usual...,
async: false, // new option
}) |
No. The API request here is like other mutation observer callbacks and only happens at the end of a current microtask. |
@trusktr I for one have argued for an asynchronous approach, partly because we know from the past how There are several reasons why observing the connectedness of any node is not trivially solved with Mutation Observers, in part because a Node may move between roots in the DOM tree. Listening for changes to child lists across every existing and future Shadow Root in the document just to track a Node is not ideal, and at least my primary motivation for wanting to see this eventually being solved by observing the node itself. |
We have a use-case for this feature, probably more of the same, but the ability to directly watch the connectedness of an element is the only feature we're missing. We have an interactions library that's agnostic to the rest of the code, and especially to frameworks, which may alter the DOM. |
Will note that while notification is asynchronous, subscription and disconnect is synchronous. And the |
@dead-claudia yes, thanks for clarifying this to prevent potential misunderstandings. I'm fully aligned with that, and I think there is strength in that it follows the same design principles as existing observers such as E.g. something like this would be ideal to me: const observer = new ConnectionObserver(entries => {
for (const {connected, target} of entries) {
console.log("target:", target);
console.log("connected:", connected);
}
});
// Observe 'someElement' for connectedness (synchronous)
observer.observe(someElement);
// Eventually disconnect the observer (synchronous)
observer.disconnect(); I made a little proof of concept of that here, which works well, but requires patching |
@wessberg That's just awesome man! Nice poc, thank you so much |
Why would we need a separate observer for connectedness? It seems to fit right in the scope of |
@LeaVerou Good question, I have an example demonstrating the complications, I will share it when prepared. |
It seems pretty straightforward and web-compatible to me to add a new |
Simply doing This code implements liveness observers for all but children of declarative shadow roots: let observeShadow
const observers = new Set()
const registry = new FinalizationRegistry(held => {
observers.delete(held)
})
const oldAttachShadow = Element.prototype.attachShadow
Object.defineProperty(Element.prototype, "attachShadow", {
value() {
const root = oldAttachShadow.apply(this, arguments)
for (const weak of observers) {
const observer = weak.get()
if (observer) observeShadow(observer, root)
}
return root
},
})
class LivenessObserver {
#callback
#observed = new WeakSet()
#observer = new MutationObserver(records => {
this.#check(records)
})
static {
observeShadow = (observer, root) => {
observer.#observer.observe(root, {
childList: true,
subtree: true,
})
}
}
constructor(callback) {
this.#callback = callback
observeShadow(this.#observer, document.documentElement)
const weak = new WeakRef(this)
observers.add(weak)
registry.register(this, weak)
}
#check(records) {
for (const r of records) {
for (const n of r.removedNodes) {
if (!this.#observed.has(n)) continue
try {
this.#callback.removed?.(n)
} catch (e) {
reportError(e)
}
}
for (const n of r.addedNodes) {
if (!this.#observed.has(n)) continue
try {
this.#callback.added?.(n)
} catch (e) {
reportError(e)
}
}
}
}
observe(node) {
this.#observed.add(node)
}
disconnect() {
this.#observed.clear()
}
} Will note that this is probably not as fast as it could be. |
Back to the original feature request, I propose a minimal compromise: add a Chromium, Firefox, and Safari all three track this via a bit flag on the node itself. And it already needs recursively checked anyways to invoke custom element reactions, so it's not hard to just add a stack of mutation observers to possibly invoke with each node during the pass. Unobserving nodes is better discussed in #126. Please, let's keep this on-topic, to avoid spamming everyone's notifications. (I'll admit some fault in contributing to the problem.) |
@justinfagnani Maybe I got you wrong, is this how it should work? const observer = new MutationObserver(entries => console.log(entries[0].connection))
const node = new Text("123")
observer.observe(node, { connection: true })
document.body.append(node) // => true
node.remove() // => false Then I disagree. I personally think that connection is not mutation of a connected node (but a parent or document), it would be ambiguous to add it this way. The current So if it's already doing Ofc, this is not a crucial discussion, so if it's easier to implement any other way - ok. And I agree we first need to get some use-cases and application examples listed before thinking much about APIs. |
@FrameMuse His And connection is a direct consequence of mutations, just not of the element itself. Consider that connection is when iframes and images start their initial load. |
I guess this is what makes it indirect. This is what I was referring to - indirect mutation observing.
I don't think the connection events should fire when HTML is parsed, only when connections done to existing DOM in memory. |
@FrameMuse I'm talking about script-created elements here. These are called on dynamic insertion as well as during initial HTML tree building. And here's the part for iframes I was referring to. I'm also expressly just asking for a way to observe that in-practice browser bit flag, so while in theory it's indirect, it in practice is very much direct observation of a node's mutable state. |
Could Nodes simply emit non-bubbling "connect" and "disconnect" events? That would let us do everything that currently requires |
We've been down that path before with MutationEvents which caused many issues and was later unshipped from browsers. Instead I think we may be able to achieve consensus that adding a |
@dead-claudia Yeah, it depends how you think about it. So let's say we both understand each other. I'm just saying adding ~ @wessberg I don't really mind for now, we can think about better place after it's shipped to browsers and countless experiments. |
@jamesdiacono AFAIK, |
It would be useful to have a way of observing when a node gets connected or disconnected from a document similar to the way
connectedCallback
anddisconnectedCallback
work for custom elements but unlike those at the end of the microtask like all other mutation records.The text was updated successfully, but these errors were encountered: