WebWeWant / webwewant.fyi

If you build websites, you inevitably run into problems. Maybe there’s no way to achieve an aspect of your design using CSS. Or maybe there’s a device feature you really wish you could tap into using JavaScript. Or perhaps the in-browser DevTools don’t give you a key insight you need to do your job. We want to hear about it!
https://webwewant.fyi
MIT License
76 stars 23 forks source link

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

Open WebWeWantBot opened 3 years ago

WebWeWantBot commented 3 years ago

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:

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


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

bkardell commented 3 years ago

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:

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:

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 commented 3 years ago

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 https://github.com/WebWeWant/webwewant.fyi/issues/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 commented 3 years ago

If these are specifically the use cases, then I believe you can see some related history here in the links in https://github.com/w3c/csswg-drafts/issues/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 commented 3 years ago

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

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

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:

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.