Skip to content
This repository has been archived by the owner on May 28, 2024. It is now read-only.

Non-boolean states #4

Open
tkent-google opened this issue Jan 31, 2020 · 58 comments
Open

Non-boolean states #4

tkent-google opened this issue Jan 31, 2020 · 58 comments

Comments

@tkent-google
Copy link
Collaborator

See https://wicg.github.io/custom-state-pseudo-class/#ex-non-boolean-state

If the feature needs to support non-boolean states, probably

  • ElementInternals should have stringStates IDL attribute
  • The attribute should support methods like _internals.stringStates.add("readyState", "complete");
  • :state() pseudo-class should support syntax like :state(readyState=complete)
@tabatkins
Copy link

  • How do the boolean and non-boolean features interact? If I set .stringStates.set("readyState", "complete"), does :state(readyState) match?

    I think the answer should be "no". CSS has several examples of pseudo-classes that exist in only a functional syntax; :lang is a syntax error. Same should apply here.

  • This allows only single-state matching. CSS has more complex examples of functional pseudo-classes. Some are more complex than we'll want, but others seem reasonable, like the ability to match several state values at once. (For example, the now-removed :drop() pseudo, whose argument could match any of "active", "valid", or "invalid", and you could match on multiple at once like :drop(active valid).)

    I think we should allow sequence<DOMString> as well, indicating all the state values that are currently set. This also makes forward-compatibility better for authors; they can do things like renaming states over time, retaining the old states for compatibility. It also allows useful things like subdividing a readyState value into more fine-grained states, allowing users to match on either the course- or fine-grained state, depending on what they want.

    Unsure if we should let users specify multiple keywords; I'm inclined to say no, since there are multiple possible semantics (must match all, like :drop(), or must match any), and you can get those semantics manually already. (Match-all can be done by writing :state(foo=bar):state(foo=baz); match-any can be done by :is(:state(foo=bar), :state(foo=baz)).)

  • We should allow matching of values with either idents or strings; that is, .stringStates.set("foo", "bar") should be matchable with either :state(foo=bar) or :state(foo="bar"). This allows people to set arbitrary values that might come from a third party, without either converting them into an ident-friendly form, or requiring users to use CSS escapes. (We have precedent for this with :lang(), which allows strings so you can write :lang("*-Latn") rather than :lang(\*-Latn), and in properties like font-family.)

  • We should plan for the eventuality of offering more complex matching via callbacks; there's no way to expose a state like :lang() currently, where you can have wildcard-matching. This doesn't have to be addressed right now, but we should keep it in mind.

  • Bikeshedding the name: I don't like stringStates. ^_^ Maybe complexStates, or stateValues, or stateMap? We use the "Map" suffix on some things in TypedOM already, so that might be considered reasonable precedent? Unsure.

  • The API should obviously be a WebIDL maplike as well, rather than sticking close to DOMTokenList which is set-like.

@domenic
Copy link
Collaborator

domenic commented Mar 4, 2020

I still question whether this feature has many use cases. None of the existing pseudo-functions match states, and no libraries have desired to add this. See w3ctag/design-reviews#428 (comment) for the direction I'd rather see, which is not connected to custom elements at all.

As such I think design work here is a bit premature.

@tabatkins
Copy link

Valid; I'm mainly doing some design work here to satisfy the CSSWG's concern that the potential extension of boolean into non-boolean is reasonable. I think avoiding this in the core spec right now is the right move.

@tabatkins
Copy link

tabatkins commented Mar 4, 2020

Your feedback in the TAG review also makes the point more forcefully that a lot of the functional pseudo-classes that currently exist are not just attribute-like, with a name/value mapping, but rather do some complex gymnastics on both the argument and on the element to figure out matching. (Really, the only example close to this simplicity was :drop(), which is why I used it.)

That said, several of our pseudo-classes could very reasonably have been done in this manner instead, such as :current/:future/:past instead being a :time(current | future | past) pseudoclass, or :link/:visited/:any-link/:local-link instead being :link and :link(visited || unvisited || local), or all the input pseudos under an :input() umbrella. Were we to design those fresh today, that's probably the exact design we'd adopt, in fact.

That sort of namespacing is nice conceptually for authors: it groups related things together so it's easier to understand what it applies to; it avoids semantic clashes like :empty vs :blank (:input(empty) would have been a lot clearer); and it encourages slightly better hygiene surrounding exclusive states (we wouldn't have made the mistake where :read-only is :not(:read-write) and thus matches non-input elements if they were instead :input(read-only) and :input(read-write)).

So, I do think there's useful space here to explore for an attribute-like state pseudo-class, in addition to more complex Houdini-driven custom pseudo-classes.

@domenic
Copy link
Collaborator

domenic commented Mar 4, 2020

Were we to design those fresh today, that's probably the exact design we'd adopt, in fact.

Hmm, I'd find that pretty confusing as an author, personally. All existing pseudo-functions are universal, but then the CSSWG starts introducing new pseudo-functions which are state-like? Just when I got used to states all being normal, non-functional pseudo-classes?

Separate from the design of custom pseudo-classes and pseudo-functions, I'd encourage consistency going forward for future built-in things. Namespacing via dashes makes more sense to me than namespacing via functional notation.

@tabatkins
Copy link

If we designed them today, we wouldn't have them as precedent for doing them a different way. ^_^

@plinss
Copy link

plinss commented Mar 11, 2020

I'm OK with the attribute remaining simply states, however I really don't think a DOMTokenList is the right fit. There's no actual token list to represent so it's a bit of an impedance mismatch, and it's not friendly to future extension.

Let's simply make it a real maplike (not set-ish like DOMTokenList):

interface CustomStates {
    maplike<DOMString, DOMString?>;
};

partial interface ElementInternals {
    [SameObject] readonly attribute CustomStates states;
};

If we want to be a bit more setlike we can add void add(key DOMString), which would be equivalent to set(key) (or possibly set(key, null)). AFAICT the only difference between being actually setlike is the value returned by the @@iterator.

:state(foo) (or :--foo) can simply test has('foo'), though if we decide to force set('foo', 'bar') to only match :state(foo=bar) (or :--foo(bar)) and not :state(foo) (or :--foo) we can make :state(foo) test for has('foo') && (undefined === get('foo')) or null === get('foo').

This supports non-boolean states while keeping very similar API ergonomics to DOMTokenList and can easily be expanded to non-string types (including callback functions, though I don't think calling back to user code during selector evaluation is going to be a good idea unless we can severely constrain the callback).

I don't have a strong need to support non-boolean states in V1, but frankly I also don't see a good reason not to. It's not like it adds a ton of complexity, and there are use cases, though I accept they're not the most common. Building an API that's geared towards booleans now at the expense of extensibility is just going to bite us down the road and create yet another awkward API when it's inevitably extended. Let's please stop repeating the same mistakes.

@domenic I think you're making an artificial distinction between pseudo-classes and pseudo-functions (which isn't actually a thing). Pseudo-classes can have a simple string, or a functional syntax. They can also match state or be used to perform selector math, these concepts are orthogonal (and given a time machine, I'd change the syntax of those like we changed pseudo-elements to be different). There are examples of both functional and non-functional notation being used for both already in CSS.

No one is proposing using this feature for adding selector math, if we ever go down that road it'll have to be a very different approach. There is nothing preventing adding additional state matching pseudo-classes that use functional notation. :dir() and :lang() are current examples, though as @tabatkins pointed out :lang() does more than simple string matching.

@tabatkins random thought, not sure if this is a good idea, but one way to get :lang()-like functionality would be to set a regex as the state's value. Then a simple regex match could determine if the selector matches, e.g. set('--lang', /en.*/) would match :--lang(en-us)...

@tabatkins
Copy link

interface CustomStates {
    maplike<DOMString, DOMString?>;
};

This makes the decision of whether the pseudo-class is a function or not a consequence of whether it's currently set to a value. Either it's impossible to represent a pseudo-class with empty arguments (like :--foo()), or we implicitly make :--foo() and :--foo equivalent in all cases. (I don't think it affects anything in CSS currently, but we have had designs who cared about it.)

It also means that either we have to make the signature maplike<DOMString, null> currently (awkward), or we have to define how states work immediately and can't start with boolean-only.

If we instead use DOMTokenList for now, and later add a map, it means we can distinguish between :-foo (defined in .states) and :--foo() (defined in .statesWithValues with an empty value), and lets us push forward with boolean work without depending on non-boolean design being completed.

random thought, not sure if this is a good idea, but one way to get :lang()-like functionality would be to set a regex as the state's value. Then a simple regex match could determine if the selector matches, e.g. set('--lang', /en.*/) would match :--lang(en-us)...

Strong dislike. ^_^ Anything more complicated than simple keyword matching should be done in a Houdini API, I think.

@plinss
Copy link

plinss commented Mar 11, 2020

This makes the decision of whether the pseudo-class is a function or not a consequence of whether it's currently set to a value.

Right, by design. We can alternatively have the non-function psuedo-class syntax match whenever the state is present in the map, regardless of its value. I think that would actually be the better approach.

Either it's impossible to represent a pseudo-class with empty arguments (like :--foo()), or we implicitly make :--foo() and :--foo equivalent in all cases. (I don't think it affects anything in CSS currently, but we have had designs who cared about it.)

If we really need to distinguish between :--foo and :--foo() (and be able to explicitly match :--foo() only) we can make the empty function match an empty string for the value and the non-function syntax match null (or just match any value as I said above).

It also means that either we have to make the signature maplike<DOMString, null> currently (awkward), or we have to define how states work immediately and can't start with boolean-only.

Which I also proposed, defining how string state values work isn't hard. Let's just do it. So far we've spent significantly more time discussing whether or not to do it than it would have taken to do it.

We can also add void add(DOMString key) and make it equivalent to set(key, null) to make the API non-awkward for the boolean use case.

Another alternative would be to make the map signature maplike<DOMString, (boolean or DOMString)>, have void add(DOMString key) be equivalent to set(key, true), and define a true value to match the non-function syntax and a false value be equivalent to not being present.

If we instead use DOMTokenList for now, and later add a map, it means we can distinguish between :-foo (defined in .states) and :--foo() (defined in .statesWithValues with an empty value), and lets us push forward with boolean work without depending on non-boolean design being completed.

And having states defined in both .states and .statesWithValues is also quite awkward, more so than a map with null (or boolean) values IMO. It makes authors deal with two different properties with two different types. Ick, why?

random thought, not sure if this is a good idea, but one way to get :lang()-like functionality would be to set a regex as the state's value. Then a simple regex match could determine if the selector matches, e.g. set('--lang', /en.*/) would match :--lang(en-us)...

Strong dislike. ^_^ Anything more complicated than simple keyword matching should be done in a Houdini API, I think.

I'm actually not a fan of it myself, but thought I'd throw it out there. Food for thought if nothing else. I'd also prefer a Houdini API, but adding user JS code to selector matching is going to be a hard sell, at least a regex is bounded. It's also not clear if lang-like/regex functionality is needed at this point.

@tabatkins
Copy link

Which I also proposed, defining how string state values work isn't hard. Let's just do it. So far we've spent significantly more time discussing whether or not to do it than it would have taken to do it.

No, see my own explorations up above, about a syntax that allows multiple tokens (a la our defunct :drop() and :local-link() pseudo-class proposals). The design work for functional pseudo-classes is not finished!

Not to mention, once we know we're doing functional pseudo-classes, we'll want a plan for handling more complex ones in the future; "Houdini will handle it" isn't an answer. ^_^

[exploration of alternative syntax shapes]

A half-map-half-set API would be novel to the web platform so far, and I don't want its design to be the result of a quick and dirty syntax exploration in an issue not dedicated to the matter. If we produce this now, we'll probably want to use it in other things in the future, and so I want to make sure we get it right.

@domenic
Copy link
Collaborator

domenic commented Mar 11, 2020

Which I also proposed, defining how string state values work isn't hard. Let's just do it. So far we've spent significantly more time discussing whether or not to do it than it would have taken to do it.

I would strongly object to adding such functionality to this API for the reasons I've stated previously, about how I think they belong separate and the pseudo-functions should not be connected to custom elements.

@tabatkins
Copy link

I strongly object to the characterization of your objection (I agree with @plinss that CSS does not draw any significant distinction between functional and boolean pseudo-classes, and we shouldn't invent such a distinction for custom elements), but we can at least agree that we shouldn't add that to the API right now. ^_^

@domenic
Copy link
Collaborator

domenic commented Mar 11, 2020

I guess we'll have that debate if it becomes important, but I'll assume for now that for one reason or another we'll avoid tying custom pseudo functions to custom elements, regardless of the reason.

@plinss
Copy link

plinss commented Mar 12, 2020

No, see my own explorations up above, about a syntax that allows multiple tokens (a la our defunct :drop() and :local-link() pseudo-class proposals). The design work for functional pseudo-classes is not finished!

I never said it was finished, just that it's not rocket science and isn't a significantly hard problem to force a bunch of smart people to stop working on it and give up.

All I've ever been asking for here is a reasonable amount of effort to explore alternative APIs that are more future proof than a DOMTokenList. Aside from this conversation I've yet to see that. Let's keep it going.

Not to mention, once we know we're doing functional pseudo-classes, we'll want a plan for handling more complex ones in the future; "Houdini will handle it" isn't an answer. ^_^

I agree. Let's give it some thought.

A half-map-half-set API would be novel to the web platform so far, and I don't want its design to be the result of a quick and dirty syntax exploration in an issue not dedicated to the matter.

What I proposed isn't really a half-map-half-set, it's just a map with one convenience method (and you yourself suggested a maplike above).

I'm also not proposing we all accept a quick and dirty exploration without giving it all due consideration. I'm just trying to start a conversation here that I've been asking to have happen for a long time. I'm more than open to alternative proposals or improvements/iterations of mine.

If we produce this now, we'll probably want to use it in other things in the future, and so I want to make sure we get it right.

Agreed. Let's do that.

@rniwa
Copy link

rniwa commented Mar 24, 2020

Given HTML has boolean attributes yet we don't have two DOM API for accessing boolean attributes and non-boolean attributes, I don't see why we want to have two different APIs for boolean states and non-boolean states.

It would be inconsistent with the rest of Web platform, and it's really awkward having to iterate over two different properties in order to process boolean vs. non-boolean, and

@tkent-google
Copy link
Collaborator Author

I think we don't need to stick with DOMTokenList. It's ok to change from it if there is an alternative easy-to-define / easy-to-implement idea.

Another alternative would be to make the map signature maplike<DOMString, (boolean or DOMString)>, have void add(DOMString key) be equivalent to set(key, true), and define a true value to match the non-function syntax and a false value be equivalent to not being present.

I prefer maplike<DOMString, boolean> in the initial version, and maplike<DOMString, (boolean or DOMString)> if needed in the future.

We can disable the maplike-provided set(DOMString, boolean) by adding our own set(DOMString).
https://heycam.github.io/webidl/#es-map-set

@tabatkins
Copy link

If people are cool with the boolean Map, then I won't object; I just thought it felt clumsy, but it's not a killer objection. Happy to go with consensus here.


So that still brings up the further question: when are parentheses allowed?

First, from a syntax perspective, we have to allow parens in all cases. We'll be under the same constraints as registered custom properties, where we don't want to have to re-do parsing after each new definition. So all custom pseudo-classes, boolean or functional, will be syntactically valid; the registration just determines whether they match or not.

We just need to decide whether we automatically make matching choices for the author based on their registration, or not.

For booleans, we have a simple choice:

  1. :--foo() is automatically non-matching.
  2. :--foo() is automatically equivalent to :--foo.

Passing arguments, like :--foo(bar), should always fail to match automatically.

For functional, we've got more choices:

  1. :--foo is automatically non-matching.
  2. :--foo is automatically equivalent to :--foo(); whether that matches or not is up to the registration, as usual.
  3. Whether or not :--foo matches is an option for the registration; if allowed, it's equivalent to :--foo().

I think only b2+f1 are inconsistent; the other five possible combinations are reasonable.


Current CSS pseudo-class design points toward b1, as none of the boolean pseudo-classes can be written in functional form; any of the f options are possible, since none of the functional pseudo-classes are valid to use with no arguments, so any of the behaviors could potentially be what they'd use in such a case.

However, @fantasai and I have created designs in the past which would require f2 or f3: the aforementioned :drop()/:drop pseudo-class, and a :local-link()/:local-link pseudo which operated similarly, where passing no arguments was a reasonable default choice, and the non-functional version was equivalent to the no-arg functional version.

@hober pointed out that consistency with HTML attribute design points to b2+f2, as boolean attributes are equivalent to passing an empty value; <div foo> and <div foo=""> are equivalent, whether the attribute was designed to be boolean or not.


Personally, I think I prefer b1+f2, tho I would be fine with anything that wasn't f1.

I prefer b1 because of consistency with existing CSS, and because something being boolean or functional seems like an interface decision that the component user should be familiar with if they're using it. The presence of parens implies that something is valid to put within them, and if that's not on the table at all, I think we should reflect that in the allowed syntax.

I prefer f2 over f1 because I've come up with designs that want to allow boolean form being equivalent to functional with no arguments, and preventing that up-front seems annoying and limiting. I prefer f2 over f3 because I don't see a good reason for a component author to create a pseudo-class that allows an empty argument, but doesn't want the boolean form to be equivalent to that. I can't write good guidance on when one should turn such an option on or off, so I don't think we should offer the option at all; confusing or unclear switches are bad API design.

@domenic
Copy link
Collaborator

domenic commented Mar 25, 2020

My objection to any API which treats custom psuedo-classes and custom pseudo-functions the same remains, including using a map-type API or allowing function syntax to count as the equivalent state syntax.

@tabatkins
Copy link

Both of those are pseudo-classes; are you trying to draw a distinction more aggressively than CSS itself does? CSS doesn't assert any fundamental difference between :invalid, :nth-child(), and :lang().

@domenic
Copy link
Collaborator

domenic commented Mar 25, 2020

I am trying to draw the same distinction that CSS currently does in practice, even if the CSS specs do not currently use two different terms. The existing CSS specs have two very distinct classes of things, which act differently and are used in different ways, and we should preserve that going forward.

I understand in the past you have worked on designs to use the pseudo-function syntax for states, which would blur the distinction, but thankfully those never shipped, so the distinction remains.

@tabatkins
Copy link

I assure you that CSS does not draw such a distinction in practice or in theory.

It happens to be the case that none of the currently-existing functional pseudo-classes are defined in such a way that passing no arguments is a reasonable thing to do. That's an accident of history, not an intentional choice we've made. The boundaries of CSS design in general would be fine with such a thing existing. Unlike languages like JS, where there's a meaningful distinction between a function and its return value, in CSS the arguments are just additional clarification to the operation of the pseudo-class as a filter.

@domenic
Copy link
Collaborator

domenic commented Mar 25, 2020

I think it's an important part of the mental model of web developers.

That aside, I don't think custom elements should be controlling selector functions operating over them. That should be another API. Custom elements should be able to set custom boolean states on themselves, which are exposed to CSS in some way. (I don't care if the way they are exposed is spelled :--foo or :state(foo); that makes sense to leave up to CSSWG folks.) Designing a function-processer is a much bigger undertaking and should stay separate from designing a boolean-state toggling mechanism, and IMO should not be tied to custom elements at all.

This is doubly important because no web developers have asked for the custom functions capability, nor do we have any use cases presented where it would be helpful. We should keep the design focused on use cases, not some idea of theoretical symmetry based on the fact that CSS specs uses the same word for two different syntaxes and mental models.

@tabatkins
Copy link

This distinct mental model exists only with you, I think. :dir(ltr) could have been spelled :dir-ltr; :playing could have been spelled :media(playing) (with :media as a shorthand/lang-generic selector for :is(audio, video)). :lang() has be spelled that way because of its wildcarding functionality, but if we ignored that, :lang-en-US/etc would have been fine. "There's a few states" vs "there's a lot of states" is not a qualitative difference separating boolean designs from functional designs.

For things that have an existing direct boolean/functional correspondence, :nth-child(1) is also spelled :first-child, :nth-last-child(1) is also spelled :last-child, and :nth-child(1):nth-last-child(1) is also spelled :only-child. The names chosen for the functional versions do not lend themselves to being used in a boolean fashion (:nth-child, by itself, doesn't indicate that it's matching the first child, or anything at all), but that's the only reason we have that distinction there.

The aforementioned abandoned proposals from Elika and I used named that were appropriate on their own and had reasonable empty-arg semantics. @annevk (among others) has proposed a :heading pseudo-class to match :is(h1, h2, h3, h4, h5, h6), which could also be invoked as :heading(1,2,3) to select a subset of heading levels (that can be aware of the HTML outlining algorithm, something nearly impossible to do with plain selectors!).

These are all reasonable things to do, and there's nothing fundamental about the syntax that would suggest we should instead, say, have a :heading and :heading-levels() distinctly-named pair for such cases.

@tabatkins
Copy link

Designing a function-processer is a much bigger undertaking and should stay separate from designing a boolean-state toggling mechanism.

This is correct, and why we're not currently designing such a thing. We're just making sure that the current design is future-friendly to functional pseudo-classes, and a maplike that accepts some sort of object representing a registration works just fine there.

(The existing Houdini APIs that register stuff do so by hiding the map in a global and just giving you the ability to add/remove from it indirectly, but fundamentally it's still just a map sitting there, holding name->registration pairs. We could match that here by hiding the map in the shadow tree and giving you indirection mutation APIs, but that seems unnecessarily obtuse when we want to ensure you have an easy way to toggle boolean states, at least; we'd just end up reproducing a bunch of API that maps/sets already have.)

@domenic
Copy link
Collaborator

domenic commented Mar 25, 2020

We're just making sure that the current design is future-friendly to functional pseudo-classes, and a maplike that accepts some sort of object representing a registration works just fine there.

I think that's incorrect, because I think the design should be fundamentally different for function-matching vs. setting boolean states.

@tabatkins
Copy link

Do you have reason to suspect that the design for handling functional pseudo-classes (presumably delegating to a worklet, like other similar things) would be fundamentally different than the existing designs for Custom Layout and Custom Paint, and the planned designs for Custom Functions and Custom Properties (v2)?

All of those are just a name->registration map, we just don't expose the map directly. (At least partially because we don't expect there to be a good reason to manipulate the map besides adding registrations and perhaps removing ones you put in, and no good reason to iterate the map either. The same is likely true of the registrations for functional pseudos, but boolean pseudos will want easy manipulation of the value.)

If you do have such a reason, hearing it would be great. ^_^ Otherwise I'm going to proceed on the assumption that the design will be similar, and a Map is appropriate.

@domenic
Copy link
Collaborator

domenic commented Mar 25, 2020

I think none of custom functional pseudo-classes, custom layout, or custom paint, should be tied to ElementInternals, like custom element boolean states are.

@tabatkins
Copy link

Oh, that's not the direction I thought you'd take.

So you think that custom elements should be disallowed from exposing information in a way similar to :lang()? Can you elaborate on why?

@domenic
Copy link
Collaborator

domenic commented Mar 25, 2020

Because I think element states are fundamentally different from things like :lang(), which operate on a document tree and don't require knowledge of the internals of an element.

@domenic
Copy link
Collaborator

domenic commented Mar 26, 2020

If so, can you elaborate on why you think "internal states of the element" are qualitatively prevented from forming a large unwieldy set? What is preventing that, or at least making it unlikely enough that we should design the API to not accommodate it?

The fact that it has not happened for any element in the history of HTML so far, nor has it happened for any custom elements created by web developers participating in these threads, nor has anyone been able to come up with such an example.

@plinss
Copy link

plinss commented Mar 26, 2020

The fact that it has not happened for any element in the history of HTML so far, nor has it happened for any custom elements created by web developers participating in these threads, nor has anyone been able to come up with such an example.

  1. The entire point of custom elements is to enable authors to do things that HTML has not, and HTML is a somewhat limited set of elements, so HTML history is irrelevant here.

  2. The custom element developers participating in these threads is not an exhaustive list. There is a long tail here that will only get longer as more people begin using what is still a relatively new technology.

  3. There have been several examples of non-boolean states, from a simple tri-state checkbox, to gauges with various warning levels, etc. Yes, those could be represented by a larger collection of booleans, but so can literally everything else, and pretty much all programming languages have non-boolean types for author and user ergonomics.

Even if, should we find ourselves 10 years out with no one, ever, having used a non-boolean custom state, a map-like API with a simple set('foo') delete('foo') is not such a horrible thing that we'd ever feel compelled to change it. Yet a token list API obviously breaks in non-friendly ways should we want to go beyond booleans. This is a simple risk/reward decision.

@domenic
Copy link
Collaborator

domenic commented Mar 26, 2020

HTML history is irrelevant here.

I fundamentally disagree with this. The point of custom elements in general, and this feature in particular, is to achieve HTML parity.

I'm not sure how to cross such an ideological gap with you, so I won't wade in further.

@plinss
Copy link

plinss commented Mar 26, 2020

I fundamentally disagree with this. The point of custom elements in general, and this feature in particular, is to achieve HTML parity.

So custom elements should never be able to do anything HTML can't already do? And are not meant to extend the platform in any way whatsoever? Then why do they exist? They seem like an awful lot of work for some syntactic sugar.

(And by the way, there's nothing preventing HTML elements from expressing non-boolean state via pseudo-classes.)

Yeah, that's quite a gap.

@domenic
Copy link
Collaborator

domenic commented Mar 26, 2020

Please do not rephrase others' words in intentionally misleading ways.

I will no longer be participating in this thread given the conduct exhibited therein.

@plinss
Copy link

plinss commented Mar 26, 2020

Please do not rephrase others' words in intentionally misleading ways.

I did not. I characterized my understanding of what you wrote. If that understanding is incorrect, please rephrase.

@plinss
Copy link

plinss commented Mar 26, 2020

We can disable the maplike-provided set(DOMString, boolean) by adding our own set(DOMString).

I don't have a strong objection to redefining set(DOMString) to being equivalent to set(DOMString, true), but I have a slight preference to adding add(DOMString) with that behavior.

A normal map called with set('foo') results in a map containing { 'foo': undefined }. So redefining set(DOMString) creates a small disparity.

I think this only becomes significant depending on how we define the selector to behave. e.g. given real map-like storage, does :--foo match set('foo', false)? or set('foo', null)?

One option is to define :--foo to match any time 'foo' is present in the map, regardless of value. Then it doesn't matter what value set('foo') puts in the map. This might surprise authors who call set('foo', false) rather than delete('foo').

Another option is to make the interface set-like for now and add set(DOMString, <boolean or DOMString or ???>) and get(DOMString) later on, and then retroactively define what value add(DOMString) was putting in the map.

@tabatkins
Copy link

And I'm also not convinced that there's always an obvious thing to do when you "call" the function without arguments.

Right, there's definitely not always an obvious thing, and many (most?) functions won't have anything reasonable to do without arguments. (None of the current built-in functional pseudo-classes do!) So if your function requires an argument, passing no args will just fail to match due to grammar mismatch, same as if you passed random garbage. (And the same applies if we choose option f2, and someone uses :--foo; if your pseudo-class doesn't understand an empty argument, it'll just fail to match.)

The point of custom elements in general, and this feature in particular, is to achieve HTML parity.

This is a misstatement of the actual point, and as such can't be used to make the point you think you are.

Custom elements and the associated feature suite are meant to achieve overall interface feature parity with HTML - anything you can do to/with a built-in HTML element, you should be able to do to/with custom elements (such as invoke them with a meaningful tagname, produce them via the parser, interact with forms, appear in the a11y tree, etc).

The point of that feature parity is to allow people to use custom elements to achieve things that HTML will never do, and do so in a way that both feels natural, and interoperates with the rest of the ecosystem as smoothly as possible.

All of this is moot for this topic, of course; we're not discussing any novel syntax here. You've been, for a reason that still eludes me, trying to assert that functional pseudo-classes are restricted to a particular use-case (using surrounding DOM-tree information, rather than internal element info), presumably based on overfitting to the current set of established selectors in the Selectors 4 spec, and then using that to argue that custom elements thus will never need to use such a syntax. That's simply not true, however, as I've repeatedly said, and Peter has supported as well. Trust the experienced CSSWG members to know the history and design space of Selectors better than you, please. ^_^

@tabatkins
Copy link

tabatkins commented Mar 26, 2020

I don't have a strong objection to redefining set(DOMString) to being equivalent to set(DOMString, true), but I have a slight preference to adding add(DOMString) with that behavior.

Yeah, using .add() (from Set) rather than reusing .set() (from Map) is definitely the preferred way, I think.

given real map-like storage, does :--foo match set('foo', false)? or set('foo', null)?

(Note: both of those are set('--foo',...).)

My intuition is that it's useful for authors to make it only match when the boolean is specifically true. This allows them to more easily do toggling (x.set('--foo', !x.get('--foo'))) and conditional setting (x.set('--foo', otherBool)), neither of which exist in Set right now, and both of which are clumsier to do by hand.

(By hand, toggling is x.has('--foo') ? x.delete('--foo') : x.add('--foo') and conditional setting is otherBool ? x.add('--foo') : x.delete('--foo').)

@plinss
Copy link

plinss commented Mar 26, 2020

(Note: both of those are set('--foo',...).)

Correct, my bad.

My intuition is that it's useful for authors to make it only match when the boolean is specifically true. This allows them to more easily do toggling x.set('--foo', !x.get('--foo'))) and conditional setting (x.set('--foo', otherBool)), neither of which exist in Set right now, and both of which are clumsier to do by hand.

I'm vacillating on this. I think I can argue it being intuitive or surprising for authors either way. I'd like others on the CSSWG to chime in.

e.g. given purely string states, :--foo would be equivalent to :--foo(*) which could be useful. (not that we'd necessarily allow a literal '*' there, this is just for communication purposes.)

@tabatkins
Copy link

e.g. given purely string states, :--foo would be equivalent to :--foo() which could be useful. (not that we'd necessarily allow a literal '' there, this is just for communication purposes.)

What do you mean by * there? Is that a wildcard for anything? I'm currently running under the assumption that :--foo would be equivalent to empty args, :--foo().

@plinss
Copy link

plinss commented Mar 26, 2020

I meant it as a wildcard, e.g. :--foo would match set('--foo', 'bar') and set('--foo', 'whatever'). I'm not sure what :--foo() would do in that situation, only match set('--foo', '')?

@tabatkins
Copy link

tabatkins commented Mar 26, 2020

Another option is to make the interface set-like for now and add set(DOMString, <boolean or DOMString or ???>) and get(DOMString) later on, and then retroactively define what value add(DOMString) was putting in the map.

Oh, and unfortunately this isn't possible, or else I'd be all for it. ^_^ The default iterators for sets and maps are different; sets iterate thru their values, maps iterate thru their items ([key, value] arrays). So we do have to make the "set or map?" decision now.

I meant it as a wildcard, e.g. :--foo would match set('--foo', 'bar') and set('--foo', 'whatever'). I'm not sure what :--foo() would do in that situation, only match set('--foo', '')?

Ah, kk. I won't speculate too much on that, as we have no idea what the eventual interface for non-boolean states will be like. We'll figure that out when we get to it; there's no requirement to solve it right now.

@plinss
Copy link

plinss commented Mar 26, 2020

Oh, and unfortunately this isn't possible, or else I'd be all for it. ^_^ The default iterators for sets and maps are different; sets iterate thru their values, maps iterate thru their items ([key, value] arrays). So we do have to make the "set or map?" decision now.

(I presume you're actually replying to the 'use a Set for now' comment instead.)

Yeah, right. I knew that but forgot, which is why I proposed a Map with the addition of add(DOMString) originally. Changing the iterator would be bad.

@othermaciej
Copy link

I strongly agree with @domenic 's position here. I don't think there is a realistic use case that requires functional syntax for states.

It's true that CSS does not distinguish pseudo-classes that are states from other kinds of pseudo-classes that are, essentially, complex combinators. Likewise, CSS does not distinguish pseudo-elements that are parts of an element from other kinds of pseudo-elements. But for custom elements, the specific real needs to do the kinds of things built-in elements can are limited to states and parts respectively.

No one has presented a realistic example of a state that can't be represented with a simple tag. These states are like the states of a finite state machine. They are not functions that take input and do complex evaluation.

We should not add complexity to the web platform for hypothetical use cases, and that's all anyone has provided so far.

It's true that some enumerated sets of states could be expressed as a function that takes a small fixed set of values. But providing a second possible syntax for something is also not a good reason to add complexity to the web platform.

Finally if there need to be other kinds of extension points to pseudo-class or pseudo-element syntax, then let's evaluate those on their own merits.

@othermaciej
Copy link

othermaciej commented Mar 27, 2020

Please do not rephrase others' words in intentionally misleading ways.

I did not. I characterized my understanding of what you wrote. If that understanding is incorrect, please rephrase.

It seemed pretty clear to me that Domenic's intention was: the purpose of custom elements is to do similar kinds of things to existing HTML elements, with the same genera kinds of interfaces exposed.

It's not to let them express things in whole new ways. For example, a feature to define an element where the open tag uses square brackets instead of angle brackets would probably not be a valuable addition to custom elements.

It might be that, in some cases, we need to go beyond the way existing HTML elements have done things to enable specific other kinds of elements. But that has to be actually demonstrated, not just assumed. And it ought to be a real need, not just a gratuitous difference from how existing elements do things.

@plinss
Copy link

plinss commented Mar 27, 2020

No one has presented a realistic example of a state that can't be represented with a simple tag. These states are like the states of a finite state machine. They are not functions that take input and do complex evaluation.

CSS uses function-like syntax in a number of places that are not functions in the programming language sense of the term and do no evaluation. It's just syntax. Please stop conflating the two concepts.

We should not add complexity to the web platform for hypothetical use cases, and that's all anyone has provided so far.

Custom elements are by definition "hypothetical use cases". They are an extension point for authors to invent new elements that we haven't thought of yet. Why restrict an extension point to only what's already been done, or worse, a subset?

There are also existence proofs of pseudo-classes that reflect state that use functional syntax, namely :lang() and :dir(). Now, you're going to argue "those aren't state", and I'm going to reply "depends on how narrow you're defining 'state'". They reflect properties of the elements (and I'm using the term 'properties' in the generic sense, not meaning JS properties), another form of state.

And the "complexity" we're talking about here is simply giving custom element authors access to something CSS can already do, namely pseudo-classes with functional syntax and more than a simple boolean flag. There's no new CSS syntax. This isn't inventing something new, it's giving custom elements the ability to do something HTML elements already can. There have been numerous proposals for pseudo-classes that use functional syntax that aren't selector math.

This is also part of turning "custom state pseudo classes" into simply "custom pseudo classes" (along with the syntax change). Let authors decide what they want to use them for, and allow them to mint new pseudo-classes, just like HTML elements can.

@plinss
Copy link

plinss commented Mar 27, 2020

I did not. I characterized my understanding of what you wrote. If that understanding is incorrect, please rephrase.

It seemed pretty clear to me that Domenic's intention was: the purpose of custom elements is to do similar kinds of things to existing HTML elements, with the same genera kinds of interfaces exposed.

Perhaps you share more context with Domenic than I do. My interpretation of what he wrote was based on how I understood the words he wrote. Nothing more, nothing less.

Tab's explanation of what Domenic was likely going for makes sense to me, and I very much agree with those goals.

Let me be clear and blunt. I am not arguing for the sake of argument. I am not deliberately misrepresenting anything anyone says. I'm also not trying to "win". I may not take everything written in the exact sense in which it was meant, but all I can offer is my best effort trying to understand the sense that was conveyed. As does everyone else. Human communication is not perfect.

I am acting in good faith trying to improve the web platform. And I presume you all are too.

It's clear to me that several of you have differing viewpoints than I, and others, do. I am making best efforts to try to understand your viewpoints and take your concerns into consideration. Please do the same for me. Talking past each other is frustrating.

It's not to let them express things in whole new ways. For example, a feature to define an element where the open tag uses square brackets instead of angle brackets would probably not be a valuable addition to custom elements.

Giving an author access to a custom pseudo-element isn't a "whole new way" of expressing things. It's giving custom elements the exact same capability that HTML elements already have. That's a goal, right?

@tabatkins
Copy link

It's not to let them express things in whole new ways. For example, a feature to define an element where the open tag uses square brackets instead of angle brackets would probably not be a valuable addition to custom elements.

No need for hyperbolic examples here. Quoting myself from a few comments back:

Custom elements and the associated feature suite are meant to achieve overall interface feature parity with HTML - anything you can do to/with a built-in HTML element, you should be able to do to/with custom elements (such as invoke them with a meaningful tagname, produce them via the parser, interact with forms, appear in the a11y tree, etc).

The point of that feature parity is to allow people to use custom elements to achieve things that HTML will never do, and do so in a way that both feels natural, and interoperates with the rest of the ecosystem as smoothly as possible.

Swapping the bracket characters used by tags is not a useful characterization of what's being talked about here; that would be a completely novel functionality that doesn't exist anywhere in HTML, and without a strong justification, wouldn't be appropriate for custom elements.

What we're talking about here is whether or not custom pseudo-classes should be permanently restricted to a subset of pseudo-class syntax, or should (eventually) be allowed to use the full syntax that existing pseudo-classes use.


So far, I haven't seen good arguments for ruling out the possibility of custom functional pseudo-classes. The attempted arguments have been that (a) CSS only uses functional pseudo-classes to express "tree information" or combinator-ish things, not internal states of an element, and (b) there are no reasonable examples of internal states that would be better exposed as functional pseudo-classes rather than a set of boolean pseudo-classes.

Neither of these are correct. In the current Selectors spec, most of the functional pseudo-classes are combinator-ish (:is(), :not(), the various :nth-foo() pseudo-classes), but :dir() and :lang() aren't. They're exposing the actual state of the element - its direction and language. This can be influenced by attributes higher in the tree, but it's not an aspect of the tree's structure or just a higher-order modifier over an existing selector. More importantly, as I said in an earlier comment:

The fact that lang information is from the document tree isn't relevant here, it's the fact that doing :lang() with booleans would involve hundreds of distinct states, and require authors to write :is() selectors over many of them at once for some common tasks.

Even if you want to push the point that an element's language comes from the tree rather than being an internal state, you cannot argue that the design of :lang() flows from that. Its design is a result of the matching space being large and complex, such that exposing it with a set of booleans would be very complex both for the spec/browser (literally hundreds of boolean pseudo-classes!) and for the page author (Want to match any sort of English? Congrats, you have to write out an :is() with dozens of options to capture them all! And the set you need will expand over time, so I hope you're still doing page maintenance in a few years! Also maybe the set is actually infinite, due to -x-foo suffixes!)

So, concretely: @othermaciej, are you trying to argue that it is unrealistic that a custom element will ever want to do something remotely similar, where it wants to let authors match over a large (or infinite) set of states, so we shouldn't try to design an API that eventually accommodates it? I'm trying to get this argument to focus on the concrete questions rather than continuing to circle around abstract disagreements.

Note that I'm skipping over the multiple examples of functional pseudoclasses that have been proposed that are definitely based on internal state, which I linked to and talked about in previous comments; this response is too long already.


It might be that, in some cases, we need to go beyond the way existing HTML elements have done things to enable specific other kinds of elements. But that has to be actually demonstrated, not just assumed. And it ought to be a real need, not just a gratuitous difference from how existing elements do things.

I strongly agree, but that's completely tangential to the question at hand, which is whether custom pseudo-classes will ever want to do use the full set of syntax that existing pseudo-classes do. Nothing being discussed here has suggested going beyond current CSS syntax in any way.

@othermaciej
Copy link

othermaciej commented Apr 8, 2020

This spec is not "custom pseudo-classes", it's "custom state pseudo-class" in particular. So we don't necessarily need to expose every possible face of CSS pseudo-element syntax. Just enough to express states.

Current HTML elements often express state as a styling hook. This is generally specific to one element or a subset of elements. Mostly they are documented here and they represent a specific state of the element. https://html.spec.whatwg.org/#pseudo-classes

It's really easy to think of examples of states that aren't one of the built-in ones which might apply to novel custom elements. For example, a toggle-button custom element would surely want to express :state(toggled) (or :--togged). A custom-notification custom element would want to express :state(shown) (or :--shown).

All I'm asking for is a plausible example of element-specific state like the above which would clearly benefit from the functional syntax, to the point that a fixed set of values wouldn't do the job.

In particular, I don't think :dir or :lang are suitable examples. They are not element-specific, or about element-internal state. They are generic and about the shape of the DOM tree. If we felt it was necessary to add an extension point to add things like this, then it should be a separate CSS extension point to add a custom pseudo-class that applies to all elements, not part of an API tied to Custom Elements.

Also, an argument along the lines of "who knows what custom elements could do, they could do anything" is not, itself, a suitable example.

Also, the only examples I see in previous comments are how existing state-like pseudo-classes could be reworked to use functional syntax, but they do not explain why it would be necessary or helpful to do so. (It seems seems self-evidently not necessary).

If no one can present such an example, then I think we can presume there's no obvious use case. So far, the only real argument presented is syntax completionism, and I don't think that's a good enough reason to add complexity to this particular API.

@tkent-google
Copy link
Collaborator Author

Because I don't think non-boolean state support is mandatory, the first version of the feature and its implementations won't have non-boolean state support, and it's possible that we never add non-boolean state support.
Web developers will use the first version for a while, and they would be confused if the feature had weird syntax and API.

I'd propose the first version as:

  • :--foo, not :--foo()
  • an IDL attribute of setlike<DOMString> interface, not DOMTokenList
interface ElementInternals {
  ...
  readonly attribute CustomStateSet states;
}
interface CustomStateSet {
  setlike<DOMString>;
  // Overrides 'add()' of the setlike.
  // Throws if 'key' doesn't match <dashed-ident>.
  void add(DOMString key);
}

A future version might have non-boolean states. Non-boolean states will be supported by a different selector syntax and a different IDL attribute.

  • :--bar(value1)
    :--bar won't match to non-boolean states.
    :--bar() won't match to boolean states.
  • an IDL attribute of maplike<DOMString, DOMString> interface
interface ElementInternals {
  ...
  readonly attribute CustomStateSet states;
  readonly attribute CustomStateMap stateMap;
}
interface CustomStateMap {
  maplike<DOMString, DOMString>;
  // Overrides 'set()' of the maplike.
  // Throws an exception if 'key' exists in internals.states or 'key' doesn't
  // match <dashed-ident>.
  void set(DOMString key, DOMString value);
}
  • Make the first version simple as possible.
  • Treat non-boolean state as a separated feature.

Any objections?

@annevk
Copy link
Contributor

annevk commented May 22, 2020

I wonder how well worked out setlike/maplike are. In existing collection classes, e.g., Headers or DOMTokenList, all the methods perform argument validation, for instance.

@plinss
Copy link

plinss commented May 23, 2020

I don't object to the first part of your proposal (for the first version).

While I still feel that non-boolean states are useful, as I've said multiple times, I'm OK with version 1 being boolean only, so long as there's a reasonable path to go beyond that.

I can live with a setlike API, however I still prefer a maplike, even for V1. A maplike can still have an additional void add(DOMString key); method that is equivalent to set(key, undefined). The :--foo selector can then match whenever the key '--foo' is present, regardless of value. This would give an equivalent API and behavior to your proposal (with the exception of iteration) and provides for extensibility without adding a clunky API later. Users can use .keys() to replicate the setlike iteration behavior.

If we start with a setlike, we can still add void set(DOMString key, DOMString value); to it, but then iteration becomes weird. (Frankly I think maplike iteration should have just iterated over the keys, like Python does, which would have made this all much simpler.)

I do object to your plans for a future version for several reasons.

  1. While it's important to think about possible future API surface to design an extensible API now, it's premature to actually specify that future API. If we're going to specify the non-boolean now, let's just specify it and implement that.
  2. This is the wrong venue to specify selector behavior, that should be done by the CSSWG.
  3. I really don't like the idea of a separate state set and state map. Especially without clearly defining how they interact when the same key is set in both. If we do support non-boolean states in the future, there should be a single states object.

@tkent-google
Copy link
Collaborator Author

I wonder how well worked out setlike/maplike are. In existing collection classes, e.g., Headers or DOMTokenList, all the methods perform argument validation, for instance.

According to the current definition of setlike, we can override add() delete() and clear(), but can't override has(). I think overriding add() is enough in practice.

@tkent-google
Copy link
Collaborator Author

While I still feel that non-boolean states are useful, as I've said multiple times, I'm OK with version 1 being boolean only, so long as there's a reasonable path to go beyond that.

I still think non-boolean states is unnecessary. Probably we won't implement it in Google Chrome unless other browser vendors strongly support it.

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

No branches or pull requests

7 participants