csswg-drafts icon indicating copy to clipboard operation
csswg-drafts copied to clipboard

[css-nesting-1] CSSOM for nested media query rules

Open sesse opened this issue 2 years ago • 10 comments

What should the CSSOM look like for this snippet?

div {
  color: red;
  @media (min-width > 100px) {
    color: green;
    & span {
      color: blue;
    }
  }
}

Specifically, where does the “color: green;” go? It cannot be on the media query rule, because those do not support containing CSS properties. One could say there's an implicit & {} CSSStyleRule (the example says the syntax is “equivalent” to that, though the standard text does not specify it, just that the properties “[apply] to the same elements as the parent rule”), but if so, would the “& span” rule be a subrule of that implicit & {} rule, or would it lie under the media query rule? How would it serialize?

What about if the “color: green” is removed, so there are no properties under the @media-query? Would that change anything, or would there still be a & {} rule?

sesse avatar Oct 07 '22 13:10 sesse

I'm wondering if we should say something like: If the first non-comment token in a @media (or @supports, etc.) block is not a & or an at-rule, the entire block is implicitly wrapped in & {}. I don't have strong opinions on whether that & {} should be removed again in serialization or not.

sesse avatar Oct 08 '22 08:10 sesse

Naive questions: why CSSMediaRule.style could not be defined (if the media rule is not in a style rule, declarations must be ignored), and should it be possible (and how) to declare a property value via CSSOM for a media rule nested in a style rule?

cdoublev avatar Oct 08 '22 15:10 cdoublev

It could, but you'd end up with a pretty weird un-orthogonality, if you suddenly have rules that are to be ignored (but otherwise correctly parsed, visible through CSSOM etc.). You could of course declare something like that rules within @media at the top level have the same effect as being within the universal selector, but at that point, you should probably also declare that naked properties at the top should, and all of this sounds very much like something that should be done in the MQ spec, not in nesting…

sesse avatar Oct 10 '22 08:10 sesse

you suddenly have rules that are to be ignored (but otherwise correctly parsed, visible through CSSOM etc.)

(Sorry,) what rules should suddenly have to be ignored? I only asked if it could be defined that declarations specified via CSSMediaRule.style could be ignored when the context of CSSMediaRule is not <style-block>.

~~Rules which are invalid according to their context should also be ignored when specified via CSSMediaRule.cssText though~~ (on setting the cssText attribute must do nothing), but I think ignoring rules which are invalid according to their context is already defined normatively in CSS Syntax, and in the corresponding specs for the rules. For a conditional rule, in CSS Nesting:

[...] this specification allows nested conditional group rules inside of style rules. When nested in this way, the contents of a conditional group rule are parsed as <style-block> rather than <stylesheet>

If you meant declarations that are to be ignored, I think anything other than an at-rule appearing in a list of rules (<stylesheet>) is not ignored but consumed until the parser finds a simple block.

/* top level */
@media {
  property: value; /* consumed as the prelude of... */
  {} /* ... a (invalid) qualified rule */
  @media {}  /* parsed */
}

cdoublev avatar Oct 11 '22 07:10 cdoublev

You want to parse @media differently depending on the context it's in? I'm not sure if that's ideal either.

sesse avatar Oct 11 '22 08:10 sesse

That is what the spec wants. I have no opinion as to whether this is ideal or not, but I can imagine this can be a pain depending on the CSS parser implementation.

cdoublev avatar Oct 11 '22 13:10 cdoublev

Specifically, where does the “color: green;” go?

It'll go on a .style in CSSMediaRule (not yet defined by the spec).

You want to parse @media differently depending on the context it's in? I'm not sure if that's ideal either.

Yeah, that's the intent of the spec - top-level @media parses its body as a top-level stylesheet, while nested @media parses its body as a nested style rule's body.

tabatkins avatar Oct 11 '22 20:10 tabatkins

It'll go on a .style in CSSMediaRule (not yet defined by the spec).

The MQ spec, or the nesting spec?

What happens if you set e.g. .style.color on a CSSMediaRule that's not nesting?

sesse avatar Oct 12 '22 10:10 sesse

...If the first non-comment token in a @media (or @supports, etc.) block is not a & or an at-rule, the entire block is implicitly wrapped in & {}. I don't have strong opinions on whether that & {} should be removed again in serialization or not.

Hi @sesse

IMO like this, implicitly wrapped if it does not exist, but should probably not be remved in serialization.

div {
  color: red;
  @media (min-width > 100px) {
    & {color: green;}
    & span {color: blue;}
  }
}

At least how I am thinking of the ampersand to be like the $ in a regex rewrite, considering the & as a symbol for the parent selector.

Myndex avatar Oct 12 '22 20:10 Myndex

The MQ spec, or the nesting spec?

Not defined in either. Which it lives in when it is defined is just a question of what's more convenient.

What happens if you set e.g. .style.color on a CSSMediaRule that's not nesting?

Not defined yet, but most likely the result will be "throws an error upon setting" (or possibly "silently ignores the set", since the CSSOM does a lot of silent ignoring).

tabatkins avatar Oct 18 '22 17:10 tabatkins

We implemented the rule of implicit & {} in Blink, and I think the end result is good enough that the spec should strongly consider adopting it. It gave us:

  • Free reuse of the style declaration parser for this case
  • Instant disallowing of @layer and other relevant rules in indirectly nested context (save for in CSSOM inserts, which we'd need to check anyway)
  • Zero CSSOM extensions required
  • Instantly correct invalidation
  • No new inconsistent cases around top-level @media
  • Instantly correct handling of order tiebreak between properties and subrules
  • And most of all, no need to insert such a rule internally when constructing rule sets for matching/applying (which we'd otherwise probably need to do)

The single thing that's “odd” is that when you serialize (or peek into the rules with CSSOM), you don't get back what you wrote the first time. But this isn't new, and the serialization part could be fixed by removing & {} if it's the first rule if we really care.

sesse avatar Oct 21 '22 10:10 sesse

I’m fine with implicitly wrapping with & {} if it makes it easier to implement, but it should be removed in serialization / CSSOM per our current principle about serialization being the shortest equivalent syntax (if original syntax cannot be preserved).

Though we should explore if that would cause issues when using @media in @scope. Does wrapping in & {} there (which is equivalent to wrapping in :scope {}) change the meaning? These should be consistent.

LeaVerou avatar Oct 21 '22 15:10 LeaVerou

I’m fine with implicitly wrapping with & {} if it makes it easier to implement, but it should be removed in serialization / CSSOM per our current principle about serialization being the shortest equivalent syntax (if original syntax cannot be preserved).

I'm fine with such a rule. Say something like “if the first sub-rule is & {…}, remove that in serialization”.

Though we should explore if that would cause issues when using @media in @scope. Does wrapping in & {} there (which is equivalent to wrapping in :scope {}) change the meaning? These should be consistent.

You mean something like this, or am I misunderstanding you?

@scope (from: .foo) and (to: .bar) {
  div {
    @media {
      color: red;  // Would be wrapped in & {}
    }
  }
}

sesse avatar Oct 21 '22 16:10 sesse

In implementing this, I realized that we actually don't have any way of serializing conditional group rules that contain declarations at all. So I wrote up a spec that's intentionally very similar to the spec for serializing style rules with nested rules, and it's what I implemented in Blink. The example is for @media, but by replacing steps 1–2, we specs for @supports and so on.

Note that the current spec specifies that an empty div rule should serialize as div { }, but an empty @media rule should serialize as @media screen {\n}. I've kept this as-is, so that existing serialization doesn't change.

  1. Let s initially be the string "@media", followed by a single SPACE (U+0020).
  2. Append the result of performing serialize a media query list on rule’s media query list to s.
  3. Append a single SPACE (U+0020) to s, followed by the string "{", i.e., LEFT CURLY BRACKET (U+007B).
  4. If there is at least one rule in the rule's cssRules list, and the first rule is a CSSStyleRule with a single selector that would serialize to exactly “&”, and that rule has no children: 4.1. Let decls be the result of performing serialize a CSS declaration block on the first rule’s associated declarations. 4.2. Let rules be the result of performing serialize a CSS rule on each rule in the rule’s cssRules list except the first, or null if there are no such rules. 4.3. If rules is null: 4.3.1. Append a single SPACE (U+0020) to s. 4.3.2. Append decls to s. 4.3.3. Append " }" to s (i.e. a single SPACE (U+0020) followed by RIGHT CURLY BRACKET (U+007D)). 4.3.4. Return s. 4.4. Otherwise: 4.4.1. Prepend decls to rules. 4.4.2. For each rule in rules: 4.4.2.1. Append a newline followed by two spaces to s. 4.4.2.2. Append rule to s. 4.4.3. Append a newline followed by RIGHT CURLY BRACKET (U+007D) to s. 4.4.4. Return s.
  5. Otherwise: 5.1. Append a newline to s. 5.2. Append the result of performing serialize a CSS rule on each rule in the rule’s cssRules list to s, separated by a newline and indented by two spaces. 5.3. Append a newline to s, followed by the string "}", i.e., RIGHT CURLY BRACKET (U+007D)

sesse avatar Oct 24 '22 11:10 sesse

We implemented the rule of implicit & {} in Blink, and I think the end result is good enough that the spec should strongly consider adopting it.

Must say this change feels very natural. In fact, when the firs commits landed in Canary I started to write my CSS without an explicit & {}, only to notice that I had to add them in order to make the code work.

So that’s a +1 from me on this particular detail of the CSS Nesting feature.

bramus avatar Oct 25 '22 12:10 bramus

@bramus FWIW, the main discussion on this bug isn't whether we can write declarations directly inside a nested @media or not, but whether they will be wrapped in an implicit & {} (which is removed again on serialization) or not. (You would observe it in CSSOM, but otherwise not.)

sesse avatar Oct 25 '22 12:10 sesse

I'm fine in theory with the "implicit & {}" thing, but it's inconsistent with some existing at-rules: in particular, CSSPageRule is the one place we currently have that can mix arbitrary declarations with at-rules, and it has both .style and .cssRules.

On the other hand, it is true that the use of .style on CSSFontFaceRule, and probably on CSSPageRule, was a legacy mistake; they have a very restricted set of declarations they allow, and should have used individual JS properties per declaration, like CSSCounterStyleRule or CSSFontFeatureValuesRule. Sigh.

We also have one related issue, which is how we'll represent nested rules in style attributes (assuming we do that in the future). The el.style attribute just is a CSSStyleDeclaration, so there's no CSS*Rule to hang a .cssRules off of. Ideally, we'd have the same API shape as normal style rules. This suggests we might want to proactively move normal nested rules to hang off of CSSStyleDeclaration.

If we did so, then our rules would be:

  • For normal style rules, you access properties on .style, and nested rules on .style.cssRules.
  • For attribute styles, you access properties on .style, and nested rules on .style.cssRules.
  • For at-rules, aside from a few legacy ones, you always access their contents as .cssRules, including nested properties. Non-property declarations are always accessed as similarly-named JS properties (aside from the legacy ones).

tabatkins avatar Oct 25 '22 21:10 tabatkins

So essentially the diff is that we'd move rule.cssRules (in the current spec draft) to rule.style.cssRules? It's a bit tricky for us, but it's possible. Does this mean that getComputedStyle() also gets a cssRules? That's perhaps a bit odd.

sesse avatar Oct 26 '22 08:10 sesse

This suggests we might want to proactively move normal nested rules to hang off of CSSStyleDeclaration.

I would be interested to get links to the discussions considering the opposite: moving the CSSStyleDeclaration interface into CSS*Rule (and leave .style as legacy).

cdoublev avatar Oct 26 '22 11:10 cdoublev

So essentially the diff is that we'd move rule.cssRules (in the current spec draft) to rule.style.cssRules? It's a bit tricky for us, but it's possible.

Yeah.

Does this mean that getComputedStyle() also gets a cssRules? That's perhaps a bit odd.

Not necessarily. If we want to keep the object types exactly as current, then sure, they'd have a (null) .cssRules, but possibly we could use subclasses to separate this.

I would be interested to get links to the discussions considering the opposite: moving the CSSStyleDeclaration interface into CSS*Rule (and leave .style as legacy).

There are none, as no one has ever suggested doing this. ^_^

tabatkins avatar Oct 26 '22 14:10 tabatkins

Agenda+ to get a CSSWG resolution nesting property declarations within @media rules etc. when nested into a style rule. The three reasonable options seem to be:

  1. naked properties aren't allowed in @media; you have to use nested style rules, such as & {...}.
  2. naked property declarations are allowed in @media etc.; they're implicitly wrapped in an & {...} style rule so we don't need to add a .style accessor to the OM.
  3. naked properties are allowed in @media etc., and they're exposed via a new .style accessor.

(1) is inconsistent with established syntax from Sass and elsewhere. It also means authors have to write some additional wrapping rules for the common case where you just want to conditionally apply some properties based on an MQ or similar.

(3) is more consistent with CSSStyleRule, but requires more invasive changes to at-rule OMs. It also means that, for example, deleting everything from .cssRules from an @media (which today would completely clear it out) will leave the properties alone.

I'd prefer (2), as it requires the least changes to the OM and makes it slightly more likely that existing code manipulating the OM will continue to work as intended.

tabatkins avatar Jan 10 '23 23:01 tabatkins

Also preferring option 2.

bramus avatar Jan 11 '23 10:01 bramus

Option 2 is what we've currently implemented in Chromium, so that naturally gets my preference as well. :-)

sesse avatar Jan 11 '23 10:01 sesse

Does option 2 mean that you cannot add a single declaration to the nested @media via CSSOM except with nestedMedia.cssText?

Does option 3 mean adding .style to CSSMediaRule or does it mean adding CSSMediaRule.setProperty(), etc?

EDIT:

No one answered my questions so I give my own answers in case they might be useful to someone else...

Options 2 means you can add a declaration to a nested media query with nestedMedia.cssRules[?].style where ? is the index of the implicit nested style rule, among any other rules possibly nested in @media.

Option 3 means adding .style to CSSMediaRule. CSSMediaRule.setProperty() is not considered.

cdoublev avatar Jan 11 '23 10:01 cdoublev

It also means that, for example, deleting everything from .cssRules from an @media

cssRules is read-only.

cdoublev avatar Jan 11 '23 10:01 cdoublev

Prefer option 2

argyleink avatar Jan 11 '23 15:01 argyleink

cssRules is read-only.

Yes, so you can't assign an empty array to it, but you can certainly call .deleteRule() in a loop.

tabatkins avatar Jan 13 '23 19:01 tabatkins

Does option 2 mean that you cannot add a single declaration to the nested @media via CSSOM except with nestedMedia.cssText?

It sounds like adding a single declaration would then work via nestedMedia.cssRules[0].style.setProperty(). @tabatkins Correct?

If so, I'm for option 2, as it allows to keep the OM for CSSMediaRule clean while still providing syntax sugar for CSS.

Sebastian

SebastianZ avatar Jan 13 '23 22:01 SebastianZ

If there's already a style rule there, yeah, you can just set a property. If not, you first construct a CSSStyleRule and then .insertRule() it into the @media rule.

tabatkins avatar Jan 13 '23 22:01 tabatkins

If there's already a style rule there, yeah, you can just set a property. If not, you first construct a CSSStyleRule and then .insertRule() it into the @media rule.

It would be nice if the details of how this syntax sugar is resolved are as transparent as possible for authors. Basically, can we go with an option 2 and make it look like option 3 as much as possible? E.g. could we hang a .style on CSSMediaRule et al to provide a shortcut for creating this rule if it doesn't exist?

LeaVerou avatar Jan 14 '23 13:01 LeaVerou