Closed tkent-google closed 4 years ago
Personally I'm very happy to see this moving forward, custom states visible as pseudo classes is something that I've wanted for a long time.
I'm concerned however that this design seems to only allow boolean states. While boolean states should be simple for authors to use, I can see many use cases for other types, at the least strings and numbers.
I accept that other values opens up a number of issues that may not want to be addressed in version 1 of this proposal (e.g. do we allow for more than simple value matching, substrings, numeric comparisons?), but at the least it would be good if the api surface allowed for other types in the future. Perhaps the states attribute should be a map rather than a token list?
I'm also somewhat in favor of the :--state
syntax (or :--state(value)
for non-boolean states), but I'd also want to see it kept in alignment with ::part()
, so if this uses :--state
then custom parts should use ::--part
. (fwiw, differentiating pseudo elements from pseudo classes via ::
vs :
was intended for this extensibility point.)
@plinss Thank you for the comment.
Currently I agree with Tab's comment about non-boolean states. If we want to add more complex values, we can add something like addValue(token, value)
to states
object, and extend :state(<ident>)
to :state(<ident>=value)
in the future version.
If we want really-flexible state matching, we should apply Houdini-like extensibility to selector matching. e.g. my-element:func(myMatching) { ...}
and function myMatching(element) { return element.state == 42; }
.
(I was about to give thumbs up to @tkent-google's comment, but I'm not enthused at all about running script while selector matching. So thumbs up to the first bit.)
Using multiple booleans to represent non-boolean values can easily get out of hand as values get more complex. What about numeric states? It also doesn't have a clean path towards a future version where other state values are allowed, e.g. do I have to support both mode-collapsed
and mode=collapsed
?
I'm also not a fan of adding methods to a DOMTokenList that effectively turn it into something that's map-like when we have a perfectly serviceable map class already. The ergonomics of that API will just get weird and non-standard. We have enough of those, let's not add more.
I agree that we don't need complex selector matching in V1, I just want to see the API surface and the CSS syntax be a bit more future-proof. (FWIW, I think I prefer :state(mode collapsed)
or :--mode(collapsed)
rather than :state(mode=collapsed)
, there's no need to simply replicate attribute selectors, lets treat these like proper pseudo classes.)
I also agree that adding selectors that require script execution isn't a good answer here.
Thus far HTML has needed very few pseudo-classes that are more complex than booleans and in general folks mostly use classes and IDs which come down to booleans so I think you're overstating the case a bit. If there's enough demand for functional states of sorts I'd expect that to end up with a different API in any event.
Few is not zero, there's already an existence proof of the need for more than binary states. As people develop more custom elements it's not a stretch to imagine more uses.
The entire point of custom elements is to do things that HTML can't, so why hamstring custom elements to the least of HTML element's capabilities? The goal here is to be able to extend HTML, right?
There's also no point in replicating classes by another name. We already have the ability to set custom classes.
And why would we want two different APIs for setting custom states?
There is a section of the explainer which talks about why setting classes is not sufficient, which may be helpful to review.
Thus far HTML has needed
in general folks mostly use
Hm, this language indicates preservation of the status quo - the limited tools that have been historically provided in the platform.
I think new standards discussions should be about coming up with something for the future, and should be at least somewhat influenced by what tools real-world developers use when building web apps of non-insignificant complexity (take hints from real-world usage of frameworks like Lit, Svelte, React, Vue.js, etc. and how they treat the concept of "state").
If the argument is supposed to be to "use the platform", then the platform should provide compelling tools.
I guess I'm saying pretty much the same thing as @plinss is mentioning above too:
The ergonomics of that API will just get weird and non-standard. We have enough of those, let's not add more.
The goal here is to be able to extend HTML, right?
I suppose there is something to be said for not releasing a hamstrung first version.
Although if there would be near-future concrete plans for a more capable v2 right away from the start (a v2 like Tab mentioned as a response to my comment), then I guess that sounds ok as well.
I'd like to stress that this feature is "custom pseudo classes". It is not "general state exposure mechanism". It is not "expose arbitrary data structures for CSS selection". It is just giving custom elements the ability to do what built-in elements do: expose boolean bits of external state, in an element-controlled fashion (unlike class names), to their consumers.
There is an additional capability being discussed, which is the CSS "pseudo class functions" like :lang() or :dir(). I think that would be an additional interesting capability to explore allowing customization of. But, despite both starting with :, I agree with others who have stated that this will require a new, separate API, because it targets rather different underlying models and use cases. For example, something analogous to :lang() would likely not want to be tied to particular element states, but instead run a (JS?) function evaluating computed properties all elements, as the spec for :lang() does. I don't think such "custom pseudo class functions" would be tied to custom elements at all, most likely.
At no point did I ever propose exposing "arbitrary data structures for CSS selection", I talked about strings and numbers in addition to booleans.
And yes, I understand that this feature is about custom pseudo classes. Pseudo classes are a "general state exposure mechanism". Not all states are boolean.
:lang() and :dir() are expressions of state, the fact that they require evaluating properties of multiple elements to compute does not change that. There's no reason that evaluation would have to be done at selector matching time, it can be pre-calculated and stored in the element. Just like I'd expect any other non-boolean (and boolean) custom pseudo elements to be.
The other functional pseudo classes, like :has() and :where() are something I'd expect to see exposed through a completely different mechanism, like a houdini method for custom selectors. That's not what I'm talking about here.
Discussed further within the TAG. Given that people are currently exposing state in custom elements by using attributes, and using attribute selectors, and this feature is meant to supplant that practice, it seems like a good idea to survey existing custom elements to see what kinds of states they're exposing rather than guess... Let's try to gather some real-world data here.
To be clear, the web components work involves quite a few developers using web components day-to-day, and they've been most vocal about getting this added. (See also the issue thread pointed to in OP.)
I don't think @plinss was doubting use cases for the feature in general, but more looking to gauge whether a boolean-only solution would be sufficient.
e.g. if someone is styling a custom element based on [attr=foo]
rather than [attr]
, that would indicate that they are styling based on a non-boolean state.
As a frequent user of custom elements and shadow dom, I can attest both to the general need and relief in seeing the development of this feature. Thank you all for being involved in making it come to life.
While I agree with @plinss that supporting non-boolean states is something that should be pushed for, I believe I could wait for a phase II (rather impatiently) if implementors believe that doing so would make implementation easier or faster. I've been following the development of the web components specification for a while, so I know that doesn't always come from technical restrictions... I would also like to add another vote against the idea of running script while selector matching, please no! This feature and conversation fall so timely in relation to a recent talk about stateful styles by David Khourshid outlining the benefits of this style of interface: https://slides.com/davidkhourshid/crafting-stateful-styles#/
What would leave me most amenable to waiting for a phase II implementation of :state(mode=collapsed)
as a more wholistic proclamation as to the state of my elements would be assurances (by way of inclusion in the explainer) that multiple states would be supported by the current phase of the API. The explainer references :valid
as the sort of support that we'd expect to add to our elements, and I hope that would go further as to include :required:focus:hover:valid
, etc. Has this been considered, or rather its theoretical parallels of :state(required):state(focus):state(hover):state(valid)
or :state(required focus hover valid)
? I find it to be a rather logic and important extension to the thoughts outlined in the explainer as they relate to actual user/developer intent.
Thanks in advance for taking my thoughts on this into account. I'm very much looking forward to the introduction of this specification and would be pleased to offer any further support or report from the field that would be of benefit to that coming to pass.
As @alice said, I'm not questioning the utility of the feature (see my original comment), just the shape of the API.
FWIW, the TAG has a few developers that use and create custom elements as well :-)
I’m excited for this proposal and could see how it could reduce JS and simplify Ionic Framework components. The question about states for non-boolean is a great question, and initially I thought it should be required. However, I wasn’t able to come up with any scenarios within Ionic where boolean-only wouldn’t be sufficient. So I think it's a good suggestion to start out with boolean only, and possibly look into non-boolean states as phase 2, and evaluate if they're required.
@Westbrook yeah, just like any other CSS pseudo class, you can select on multiple of them. So :state(x):state(y)
selects something in both states x and y, while :state(x), :state(y)
selects something in either state x or y. Again, just like the built-ins :).
We can definitely add some examples using this to the explainer. Are there other existing CSS features that you think it would also be good to have examples of combining :state with?
I was predominately looking to ensure that :state(x):state(y)
and its proper syntax was outlined very clearly. In that you don't get much from :valid:valid
and in that some comments here and in other conversations had implied :state(x y)
may have been possible. Having a difinitive "this is what can be done and how you'd do it" will be a great resource to point people.
As for combined examples, it seems easy to assume that things in the realm of :hover
and :focus
, or even the positional :first-child
, should clearly work out of the box without directly stating, but including coverage for pseudo-elements :state(x)::before
and relational selectors :not(:state(x))
might prevent confusion down the road.
I'd just like to double check that @rakina, @domenic and @tkent-google noted @plinss' suggestion https://github.com/w3ctag/design-reviews/issues/428#issuecomment-542534030 about getting data demonstrating the need or lack thereof for non-boolean states.
@tkent-google and I spent some time discussing how to collect data on this subject, as well as the general problem space. Here is where we ended up:
Given that people are currently exposing state in custom elements by using attributes, and using attribute selectors, and this feature is meant to supplant that practice,
We don't think this is the right premise to be operating from. This feature is for states which depend on extrinsic factors such as user input, as the first paragraph of the explainer mentions. Component authors can't map such states to content attributes now, or do so but then end up with a fragile public interface---because component users can modify content attribute values.
To see what we mean, consider the built-in pseudo-class states :active
, :hover
, :valid
, and :invalid
, which can't be mapped to content attributes. I.e., what would aEl.removeAttribute("hover")
mean? This feature is intended for cases like that.
(Side note: there are some older pseudo-classes like :checked
which map more directly to content attributes, but newer elements like <dialog>
and <details>
, or element proposals like <std-switch>
, do not create redundant pseudo-classes that are 1:1 with content attributes, instead allowing authors to do normal attribute selectors like dialog[open] { ... }
or std-switch[on] { ... }
.)
Regardless, we respect the call for research and did a bit of looking through existing custom element control libraries to find what types of internal states they expose.
Ionic Framework doesn't need non-boolean states as @adamdbradley's comment states.
Polymer elements: We found only one candidate non-boolean state; the immediateValue of <paper-slider>
. However we don't think exposing such a continuous value as a state is reasonable. It's unlikely that people want to apply different styles for each of continuous values. Instead, a component can behave like the native <input type=range>
: it can expose a state change event, and/or provide boolean states like is-minimum-value, is-maximum-value, is-less-than-criterion for such continuous values.
So our conclusion is that we don't need to support non-boolean states and the current API using DOMTokenList
is enough. If a component author want to expose a non-boolean state, it should be converted to multiple boolean states or a state change event can be provided for maximum flexibility.
That said, we recognize that in the overall mission of allowing "custom X" features to be as powerful as "built-in X" features, there is a missing gap for X = "custom pseudo-functions" like :dir()
, :lang()
, :is()
, :not()
, :where()
, :has()
, :nth-child()
, :nth-col()
, etc.
For the reasons explained in previous posts in this thread, we don't think these should be considered as an extension of the problem space being tackled here, i.e. in the same ways as custom states that reflect element-internal properties. As shown by the use case analysis on libraries (as well as analysis of the existing built-in HTML elements/CSS pseudo-functions), custom pseudo-functions don't provide an element-specific API. As such ElementInternals is not the right place for them.
We'd expect these to fit better with the Houdini efforts to make CSS more extensible and tied to JavaScript in general. For example:
:nth-col()
that works well with that custom layout, or:where()
and :is()
do, or:is()
and :not()
do, or:dir()
, :lang()
, and :has()
do.All of these are not specific to a given custom element's implementation, but are powerful and interesting pieces of functionality that would be good to allow developers access to, as part of the general explain-CSS effort.
We hope this helps!
There are few things puzzling me regarding this proposal, and I'd like to list all points in here.
state
Even in the HTML world, components states can be defined either via classes or attributes, where attributes accept (at least) strings.
Using a DOMTokenList to cover an element state
seems like a very poor choice, especially if there is already a plan to allow :state(mode=collapsed)
in the future.
Most used frameworks though, use state
as basic JS object/map, allowing (at least) serializable data to be part of the state, so that while I agree the state should be handled internally, the developer experience I'd expect from this proposal would rather look like:
this._internals = this.attachInternals({status: "initial", uid: Math.random()});
// at any point in time ... updates *only* status, keep uid as it was
this._internals.update({status: "update"});
// at any point in time ... retrieve a value associated with the state
this._internals.get("status"); // "update"
this._internals.get("uid"); // the random number as string
// at any point in time ... replace the whole state, losing uid
this._internals.replace({status: "uid-less"});
// at any point in time, know if a state is set
this._internals.has("uid"); // false
// same as previously mentioned, for example purpose
this._internals.get("status"); // "uid-less"
this._internals.get("uid"); // null
// at any point in time, update a single entry of the state
this._internals.set("uid", Math.random());
this._internals.has("uid"); // true
Keeping the state in sync at once, and through an object, would make states handling per element much easier, and similar to what common frameworks do, only state keys that changed value should result into a CSS call/update/repaint
Benefits
get thingy()
and set thingy(...)
, components could have a single set state(obj) { this._internals.update(obj); }
and play well with template literals/JSX based componentsMap
with constrains on the key (DOMString) and eventually constrains on the value, but once we have map, such constrain on values could also be dropped, as developers don't use only strings with their dataThis from @annevk is an interesting one:
I'm not enthused at all about running script while selector matching
The current proposal bases CSS info on JavaScript classes getters (get checked
or set checked
), meaning that :state(checked)
will always invoke a function, so that I don't see, within the current proposal, why functions would not be allowed, when to retrieve states we need to define a function (accessor) anyway. However, my previous proposal would get rid of this issue all-together, because you don't need to expose getters and setters per each private state key, so the running function, within the CSS scope, happens only when, internally, the engine decides it should run, without touching JS at all.
Despite what WebKit does, there are numerous projects based on Custom Elements built-ins, plus the majority of VDom based frameworks don't care much about Custom Elements.
Confining this functionality only in classes that extend HTMLElement
means this will have an incredibly slow adoption/value per effort ratio:
:state(...)
check via classes (and it will likely poison attachInternals
native prototype method to be backward compatible)Using the suggested approach with Map could make this feature more broadly usable, and partially polyfillable, without needing to introspect classes, specially because classes might define this._internals
as this.#internals
, without exposing all keys handled by the internal class field.
What this proposal would like to solve, seems to be an issue nobody using frameworks really have.
Moreover, it's not clear how exposing this.attachInternals()
would solve conflicting states, as I believe el._internals = el.attachInternals()
could be done at any time, as well as defining getters and setters at distance, either on the element, or its specific class prototype, if it's a custom element.
As summary, I am not sure this specification, as presented, would really solve something concretely for Web FE developers, but I'd be happy to know more about the clear broken use case this feature would like to fix.
Best Regards
@WebReflection I guess there's more discussion happening elsewhere (in https://github.com/w3c/webcomponents/issues/738), where @tkent-google mentioned that they are getting ready to ship in Chrome:
If this doesn't have any significant issues, I'd like to try to ship this in Google Chrome.
The current proposal bases CSS info on JavaScript classes getters
It doesn't. The CSS engine can inspect the element's states token list's token set directly.
@karlhorky thanks for the hint, but reading through that, this is my outcome:
el.attachShadow(...)
, the el.attachInternals()
works only in certain conditions, creating conflicts with the fact that DOM APIs work with DOM nodes, period.I know none of these points are your direct fault, but I also believe it's already too late to reconsider this proposal.
@annevk about that ...
The CSS engine can inspect the element's states token list's token set directly.
It was silly/stupid of me thinking the JS would've played a role in the CSS side, but here is my quick feedback on the matter:
Thanks anyway for the clarification, it made instant sense as soon as I've read it, yet please consider above points to maybe improve the current documentation, or specification, sate.
Best Regards.
P.S. for @annevk ... the fact a getter could return true
but an el.matches(:state(thing))
could return false
is also concerning ... nothing my proposal would fix, but an easy road to inconsistent state handling.
specs shipping questioning only Googlers don't represent well what the Web/JS ecosystem needs
it would be awesome if specs would ask real-world DOM API users before landing on the browser
Agree :) I'm all for having quick consensus / shipping, but consulting real-world (external) users should be a bigger part of the process... I think standards can learn a lot from the experiences won by frameworks.
These allegations are not true. The spec is based on feedback from several component companies and browser vendors, most notably those that participated at TPAC.
The spec is based on feedback from several component companies
Who are these several component companies?
and browser vendors, most notably those that participated at TPAC
AFAIK TPAC is not representing the current Web development ecosystem though, which is why I've written what I've written.
@domenic, regardless my previous comment, I've simply spent some time trying to understand the proposal and writing down my feedback.
You can surely ignore that, but this is part of developing in the Open Source: feedback might arrive from users, and you can read, ignore, or maybe consider, some part of the feedback.
As Custom Elements user, either as independent Web developer or as employed one, since we ship CE to million active users, I wanted to raise my concerns regarding this API.
If it's too late, then I blame myself for not paying enough attention in this project activities, but since I've been invited to write comments regarding this API, I hoped it wasn't too late already.
My conclusions, beside who's being involved, and I might regret my allegations if my assumptions are all wrong, remain the same: this API doesn't help much Web developers, as it covers only marginally one use case related mostly to Shadow DOM, but it could be better, and hopefully before it lands.
Best Regards.
@WebReflection Thank you for the feedback.
The meaning of a state
The purpose of the proposal is not to help implementation of general states of custom elements. It's to provide a way to expose read-only states which can't be represented by HTML attributes. If a state of a custom element is already exposed as an HTML attribute, we don't need to apply the proposal to the state because we already have attribute selectors.
_internals.states
is an interface with which custom element implementations tell their states to browsers. Using it as a primary storage of states is not a goal of the proposal though such usage is possible.
ElementInternals
and attachInternals()
are not a part of the proposal. The proposal just depends on them. ElementInternals
is the place to provides APIs which only custom element implementations can call.
About ditching built-ins Elements
At this moment the proposal doesn't support customized built-in elements, but enabling the feature for customized builtin-elements is not difficult. We'd like to start with the smallest feature, and enabling it needs to change the behavior of existing APIs. So we'd like to defer it.
About using classes and the "collision" argument
_internals
is not states
. You seems to misread the proposal.
@tkent-google I have simply red the documentation where this._internals = this.attachInternals();
happens in the first example, and I would expect every DOM element capable of relating ElementInternals
to any DOM node.
If this works only with Custom Elements, or even worse, with CE that use Shadow DOM, this proposal is not really beneficial for the Web, as CE, and especially the unpolyfillable (in a reliable way) Shadow DOM, are not the most common primitives out there.
@WebReflection
attachInternals()
works only with custom elements, but it doesn't require Shadow DOM.
@tkent-google and where would such method exist, if not in the HTMLElement.prototype
? Is it provided at runtime only once defined through customElements
? If so, don't you think it'll be a surprise to not provide that for built-in extends, as all the entry points are identical?
@tkent-google and where would such method exist, if not in the
HTMLElement.prototype
? Is it provided at runtime only once defined throughcustomElements
? If so, don't you think it'll be a surprise to not provide that for built-in extends, as all the entry points are identical?
attachInternals()
exists on HTMLElement
interface, and it raises NotSupportedError
if it is called for non-custom elements.
https://html.spec.whatwg.org/C/#dom-attachinternals
@tkent-google thanks, that answer the first part of my question, yet the "least surprise" would be having that working for built-in extends too.
The fact we think new features only for non built-in extends, since these has shipped and work great already in Chrome, Firefox, and the new Edge, is a bit concerning, imho.
Built-in extends should be first citizens of the Web, not something to eventually think about later. Since states use a specific pseudo selector to be accessed, there's not even a possible conflict between :checked
and :state(checked)
, and people using built-ins with CEs would benefit from a consistent API in case they need/want/use state.
Best Regards.
I filed https://github.com/whatwg/html/issues/5166 for customized built-in elements.
I think this review request can be closed if TAG has no other comments.
@tkent-google please don't close TAG issues. The TAG hasn't made a determination here yet.
The CSSWG has taken up this issue and has our feedback on board. The TAG will monitor progress there and we're ready to close this for now.
こんにちはTAG!
I'm requesting a TAG review of:
Further details:
We recommend the explainer to be in Markdown. On top of the usual information expected in the explainer, it is strongly recommended to add:
You should also know that...
N/A
We'd prefer the TAG provide feedback as (please select one):