- From: Keith Cirkel <notifications@github.com>
- Date: Mon, 29 Apr 2024 02:30:38 -0700
- To: whatwg/dom <dom@noreply.github.com>
- Cc: Subscribed <subscribed@noreply.github.com>
- Message-ID: <whatwg/dom/issues/1285@github.com>
### What problem are you trying to solve? The TL;DR is: there are many good reasons to observe when a selector matches `N` number of elements, and when `N` changes. But here are some more concrete examples: ### Example 1: Observing state only exposed in CSS. Often times an elements state can only be properly observed via CSS selectors, for e.g. `:dir()` & `:lang()` can match implicit parents (ref https://github.com/whatwg/html/issues/7039 /cc @claviska), `:is(:popover-open, :modal, :fullscreen)` are the only way to observe if something is in the top layer (ref https://github.com/whatwg/html/issues/8783 /cc @straker, ref https://github.com/whatwg/html/issues/9075 /cc @sanajaved7), and with CSS CustomStates a custom element can _hide_ any number of state transitions behind the `:state()` selector without exposing equivalent observable properties from JS, such as events. Counter: It could be argued that state that is only observable in CSS should remain there (although `.matches()` seems to belie that). It could also be argued that any state exposed in CSS should have an equivalent state exposed in JS. ### Example 2: Observing when a new element is added that matches a given selector. It can be useful to simply discover when new elements come in or out of the DOM that match a given selector; for example many implementations of a "custom element lazy define" (ref https://github.com/w3c/webcomponents/issues/782 /cc @justinfagnani) seek this approach. See also a more generic proposal for `MountObserver` (ref https://github.com/WICG/webcomponents/issues/896 /cc @bahrus). Counter: Finding elements matching a selector is quite possible with `MutationObserver` but the code gets unwieldy very quickly, and it can quickly run into issues where the main thread is blocked because it's re-running querySelector on a large DOM, on each mutation. ### Example 3: Ergonomic API for attaching a behaviour to new elements as they enter/leave the page For a very long time, most of GitHub's JS was powered by the principle that we could observe elements as the enter the DOM. The https://github.com/josh/selector-observer library powered this, and it allowed developers to express a selector, which would fire a callback whenever the element count for the selector changed. This library concatenates all the given selectors into one giant querySelector (which, fun story, caused some regressions in Firefox from stylo, ref https://bugzilla.mozilla.org/show_bug.cgi?id=1422522). ### What solutions exist today? `MutationObserver` & the [selector-observer](https://github.com/josh/selector-observer) library are probably the closest. ### How would you solve it? I'd propose making a new `SelectorObserver` class which has similar ergonomics to other `*Observer` classes. You pass it a callback, and a selector, and it can lazily accumulate records and give them to the callback. Each `SelectorObserverRecord` could represent one observed selector, and `addedNodes` could represent newly matching nodes (either because they've just entered the DOM, or because they've now changed state), while `removedNodes` could represent no-longer-matching nodes (either because they've changed state, or just left the DOM): ```js const so = new SelectorObserver((records) => { for(const record of records) { const selector = record.selector; for(const node of record.addedNodes) { console.log(`${node} now matches ${selector}`) } for(const node of record.removedNodes) { console.log(`${node} no longer matches ${selector}`) } } }); so.observe({ selector: '.my-selector', subtree: true }) ``` The IDL might look a little like: ```webidl [Exposed=Window] interface SelectorObserver { constructor(SelectorCallback callback); undefined observe(Node target, DOMString selector, optional SelectorObserverInit options = {}); undefined disconnect(); sequence<SelectorRecord> takeRecords(); }; callback SelectorCallback = undefined (sequence<SelectorRecord> mutations, SelectorObserver observer); dictionary SelectorObserverInit { boolean subtree = false; }; [Exposed=Window] interface SelectorRecord { readonly attribute DOMString selector; [SameObject] readonly attribute Node target; [SameObject] readonly attribute NodeList addedNodes; [SameObject] readonly attribute NodeList removedNodes; readonly attribute Node? previousSibling; readonly attribute Node? nextSibling; }; ``` For the `:dir()`/`:lang()` example in https://github.com/whatwg/html/issues/7039 (@claviska) it'd look a bit like: ```js const so = new SelectorObserver(records => { for(const record of records) { for(const node of [...record.addedNodes, ...record.removedNodes]) { recalculateI18n(node); } } }) so.observe(myElement, ':dir(ltr)') so.observe(myElement, ':dir(rtl)'); for (const lang of supportedLanguages) { so.observe(myElement, `:lang(${lang})`); } ``` The top-layer example in https://github.com/whatwg/html/issues/8783 & https://github.com/whatwg/html/issues/9075 (@sanajaved7, @straker) it'd look a bit like: ```js const so = new SelectorObserver((records) => { for(const record of records) { for(const node of record.addedNodes) { updateAnchoredPosition(node); } } }) so.observe(document, ':modal, :popover-open, :fullscreen', { subtree: true }); ``` For the Custom Elements examples in https://github.com/WICG/webcomponents/issues/782 (@justinfagnani) & https://github.com/WICG/webcomponents/issues/896 (@bahrus) might become: ```js const imports = new Map(); const so = new SelectorObserver(records => { for(const record of records) { if (imports.has(record.selector) { import(imports.get(record.selector)).then(moduleRecord => { customElements.define(record.selector, moduleRecord.default)) }) imports.delete(record.selector); } } }) const lazyDefine = (tagName, importString) => { imports.set(tagName, importString) so.observe(document, tagName, { subtree: true }); } ``` ### Anything else? Hopefully folks don't mind the pings, but I thought it would be nice to encourage some discussion from those who have similar use-cases. -- Reply to this email directly or view it on GitHub: https://github.com/whatwg/dom/issues/1285 You are receiving this because you are subscribed to this thread. Message ID: <whatwg/dom/issues/1285@github.com>
Received on Monday, 29 April 2024 09:30:42 UTC