WICG / custom-state-pseudo-class

Former home of the custom state pseudo-class feature
Other
11 stars 7 forks source link

Non-boolean states #4

Open tkent-google opened 4 years ago

tkent-google commented 4 years ago

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

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

tabatkins commented 4 years ago
domenic commented 4 years ago

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 https://github.com/w3ctag/design-reviews/issues/428#issuecomment-549532912 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 commented 4 years ago

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

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

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

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

plinss commented 4 years ago

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.

tabatkins commented 4 years ago

By "like :lang()" I meant "exposing an API that would be difficult or annoying to do with boolean states, such as wildcarding over a large set of possible values". 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. This seems like something that an element could plausibly run into with other kinds of states. (I haven't spent time coming up with a plausible concrete example, but it doesn't seem to be something specific to querying langs, and so should generalize)

I mentioned this earlier in https://github.com/WICG/custom-state-pseudo-class/issues/4#issuecomment-604069109:

"There's a few states" vs "there's a lot of states" is not a qualitative difference separating boolean designs from functional designs.

domenic commented 4 years ago

The fact that lang information is from the document tree isn't relevant here

I think it's extremely relevant. We have two use cases:

We should design these APIs separately: the first not related to custom elements/ElementInternals, and the second based on them.

tabatkins commented 4 years ago

So you believe that there is no overlap there, that there is no case where an element wants to expose some internal states that form a large set?

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?

annevk commented 4 years ago

FWIW, I would also expect distinct APIs, mainly because a set is just so much simpler than having to support arbitrary functions. We should keep simple things simple. And I'm also not convinced that there's always an obvious thing to do when you "call" the function without arguments.

domenic commented 4 years ago

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

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

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

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

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

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

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

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

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

(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 commented 4 years ago

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

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

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

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

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

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

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.