Skip to content
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

I want to be able to listen to changes to Element.matches, similarly to matchMedia #387

Open
WebWeWantBot opened this issue May 27, 2021 · 4 comments

Comments

@WebWeWantBot
Copy link


title: I want to be able to listen to changes to Element.matches, similarly to matchMedia
date: 2021-05-27T14:32:13.591Z
submitter: Guylian Cox
number: 60afad6ddd1319252815bea6
tags: [ ]
discussion: https://github.com/WebWeWant/webwewant.fyi/discussions/
status: [ discussing || in-progress || complete ]
related:

  • title:
    url:
    type: [ article || explainer || draft || spec || note || discussion ]

Being able to exploit CSS's reactivity with JS would be incredible
Some scenarios:

  • Being able to use CSS selectors for events that don't exist in JS: :user-invalid, :focus-visible, :placeholder-shown
  • Being able to listen targetted descendant changes using :has()

If posted, this will appear at https://webwewant.fyi/wants/60afad6ddd1319252815bea6/

@bkardell
Copy link
Collaborator

bkardell commented Jun 7, 2021

I've attempted to draft a couple of different replies to this now, but every time I open it I get stuck a little in whether or not I am sure what the ask actually is. This asks specifically for an Element.matches, but it list two bullets explaining its use which are potentially pretty different. The key distinction here, I think, is that .matches requires a reference to a particular element to test. Today, (without :has), one could not say "let me know when any elements change their state of matching for this particular selector". However, with support for :has, one could effectively achieve the latter. I suppose the primary question is how desirable is the former without the latter?

There has been a long history of wanting something more like the latter and attempts to speculatively polyfill or build paths toward answers... Here are a few that come to mind:

  • (Not exactly the same use cases, but I think worth mentioning) jQuery had .live() which could let you handle some interaction related/event handler things with selectors as elements were added after the fact
  • When MutationObservers were added after long work through webapps/dom there was some idea to follow up on them in ways that would have been similar to this. Raphael Weinstein even produced https://github.com/rafaelw/mutation-summary and there was some hope of introducing something like this
  • Custom Elements are a specific version of this, but for elements only - you can register callbacks for when elements match the element name (see more in use cases below). Several libraries and discussions over the years have tackled something roughly similar ideas - a very incomplete list just to name a few I'm well aware of:
    • my own hitchjs was similar
    • @WebReflection has several takes around similar ideas, like regular-elements
    • Some post css 'hot dom' things plug in 'live' to selectors

Because there are a lot of things one could potentially do with more powers here, there are a lot of use cases and not all proposals or libraries have addressed all of them but basically I think they fall into a few broad categories of:

  • Enabling the attachment of Mixins (various ideas around this have been desired, even in standards for a while - several kinds of interfaces or behaviors are better as mixins than subclasses)
  • Enabling the polyfilling CSS features (in order to know when they should be applied/unapplied)
  • The ability to associate otherwise secondary or repitious information and do useful things. An example of this can be anything from log recording to CAS (cascading attribute sheets) proposal which effectively just allowed you plug into something and modify the element along the way, decorating it with additional attributes. It's useful but obviously doesn't fit CSS in the live profile.

We could get into listing a lot more concrete use cases I think, if we could nail down the desire(s) expressed here a little better?

@ephys
Copy link

ephys commented Jun 7, 2021

Hi :) Want author here, thank you for taking the time to respond

I'll try and clarify it a little bit by providing some real world scenarios

Float Labels

Float Label implementations can use input.matches(':placeholder-shown') to determine whether the label should be moved out of the input or not. This makes the float label behave properly for inputs that always have content displayed (such as date inputs).

The issue is that :placeholder-shown needs to be checked again every time the input changes. This works fine most of the time, but changing HTMLInputElement.value via JavaScript does not trigger any event. Being able to listen to input.matches(':placeholder-shown') would solve this issue. (this would resolve #394)

However, I think :has will also remove the need to use JavaScript for this use case in the future:

.float-label-wrapper:has(input:placeholder-shown) label {
	/* move label out of the way */
}

Displaying an input's error message when :user-invalid matches

This use case is very similar to the previous one.
:user-invalid is great, but something I'd like to do is display the input's validation message when this selector matches my input.

Like above, I can check :user-invalid every time a change, blur, or input event is triggered. But like above, it's unreliable (HTMLInputElement.value does not trigger anything), and much more cumbersome than listening to :user-invalid.

As above, I think :has will resolve this use case, as you could write the validation message to the dom but only display it if .input-wrapper:has(:user-invalid) .error-message matches.

Material-UI's ripple

Material-UI uses a JS implementation of :focus-visible to determine whether or not the ripple should be in its "focused" state as there is no focus-visible JS event.


Regarding Element.matches(':has(...)'), I couldn't think of a use case where this feature would be a good fit.

@bkardell
Copy link
Collaborator

bkardell commented Jun 7, 2021

If these are specifically the use cases, then I believe you can see some related history here in the links in w3c/csswg-drafts#1067, with a long a desire to let you express this in CSS itself (which seems your preference based on the above)... Sorry, it's hard to have a cannonical one as there have been many proposals on this, but that link is rich with links and refs you can follow back and back. Would a CSS only solution that these would provide be your preference, or would a live match callback idea on a single element still be preferable?

Note the ones I linked up above (when I thought you were looking more for JS stuff) are more like observers which wouldn't require you to have a reference to the element in the first place... not sure if that matters at all?

@WebReflection
Copy link

WebReflection commented Jun 8, 2021

Thanks for mentioning me @bkardell, and to complete the list of previous work around this topic:

  • wickedElements and hookedElements allow mixing in multiple behaviors per selector
  • regularElements mimic Custom Elements through CSS selectors
  • all of the above (and more) is based on the qsa-observer, which enables these libraries, and yet, it misses one very important functionality

That translates to the fact that all selectors are applied live when an element matches, but there's no way to trigger, notify, or change anything, when the element doesn't match anymore its state.

I could implement this at some point, but it feels wrong, or unexpected, as there's nothing in the platform similar (attributeChangedCallback, connectedCallback, and disconnectedCallback are all we have ... no selectorChangedCallback).

The elephant in the room

... is that every primitive we have to observe DOM changes is either very slow (imagine a MutationObserver for every single attribute change or any modification on any element on the page), or incapable of catching pseudo classes changes, so that even if there is a possibility to create a very convoluted solution with JS, this would mean that all observed selectors should diff from a previous state to a current one, and such observer cannot even use a WeakMap, because WeakMap doesn't allow crawling its content, so the whole architecture in JS only would be very memory-leak prone, or not very easy to handle even using WeakRef, as the callback to signal a node doesn't exist anymore, hence it won't match any selector, cannot be retrieved back (or it won't be weak), so we have literally no way to implement what I believe is being asked here.

A Possible Solution ?

What I think the HTML+CSS+JS combo misses, is the ability to be notified whenever an element gets re-painted, or better, whenever an element changes its "CSS state".

As pseudo example, and as possible MVP for implementation, I'd love to have something like the following:

CSS.observe('div.opened', (selector, records) => {
  console.log(selector); // '.opened'
  console.log(records); // a list of records
});

// a way to drop the observer
CSS.unobserve(sameSelector, sameHandler);

// there should be no way to CSS.disconnect() or CSS.clear()
// as that would be obtrusive and disaster prone across libaries

Let's see a practical example (Codepen):

<!doctype html>
<style>
div {
  height: 0;
  overflow: hidden;
}
div.opened {
  height: auto;
}
</style>
<button onclick="example.classList.toggle('opened')">open</button>
<div id="example">
Some content here.
</div>

Ideally, the previous JS would basically pass, after the first button click:

listener(
  'div.opened',
  [{target: {the_div_with_id: 'example'}, matches: true}]
);

After the second button click, as the div changed state, the listener will receive:

listener(
  'div.opened',
  [{target: {the_div_with_id: 'example'}, matches: false}]
);

Specs in a nutshell

  • whenever the CSS engine needs to apply a new state to one, or more, elements, and there is a selector observed for any of these, invoke all associated listeners (unique, like it is for listeners) passing the same amount of data: affected elements, their matching state.
  • the selector must be the same selector specified in the style/CSS counter part ... in this particular case, if we were observing just .opened, but the style contains div.opened, nothing should happens. This might simplify a lot the implementation and also enforce high specificity for observed selectors so that performance should not be affected
  • the dispatching of the change can be in bulk, asynchronous, or done in a similar way, the MutationObserver operate
  • if we want to take the MutationObserver API as inspiration instead, the list of records should have something like upgraded collection and a downgraded collection, with {target, selector} in it

I have no idea if this is explained well or it makes sense at all, but I believe a similar primitive, provided by the browser, would enable million new and better ways to augment the DOM, without needing to:

  • use multiple MutationObservers for attributes or subtree mutations
  • use a list of elements to match against a list of rules every time something change
  • make composition of behavior, use cases, and piece of logic extremely easy to deal with
  • avoid convoluted userland solutions that will be memory-leak prone, likely super slow, and all slightly different, without a standard behavior like this one

I hope this comment/idea somehow makes sense, but I'd be more than happy to expand more.

This would cover everything, including the :has case, which I don't find particular interesting, once anyone can observe any selector, so the scope of this idea goes beyond pseudo / has, it's about the CSS engine passing along elements that got updated after a specific selector, only if such specific selector was observed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants