Spec for the @at-root directive #774

Closed
chriseppstein opened this Issue May 28, 2013 · 25 comments

Projects

None yet

8 participants

@chriseppstein
Member

I'd like to nail down the exact spec for how @at-root will work.

So here's some test cases:

inline selector mode

.foo { @at-root .bar { color: gray; } }
.bar { color: gray; }

& still works when not in script mode

.foo { @at-root .bar & { color: gray; } }
.bar .foo { color: gray; }

& works when in script mode

.foo { @at-root .bar #{&} { color: gray; } }
.bar .foo { color: gray; }

@at-root can take a block

.foo { @at-root { .bar #{&} { color: gray; } } }
.bar .foo { color: gray; }

@at-root blocks only affect the nearest child selectors.

.foo { @at-root { .bar { .baz { color: gray; } } } }
.bar .baz { color: gray; }

@at-root doesn't remove selectors from directives by default.

@media print {
  @supports ( transform-origin: 5% 5% ) {
    .foo {
      @at-root {
        .bar #{&} {
          color: gray; } } } } }
@media print { @supports ( transform-origin: 5% 5% ) { .bar .foo { color: gray; } } }

@at-root in block form can remove a selector from all directives.

@media print {
  @supports ( transform-origin: 5% 5% ) {
    .foo {
      @at-root without-directives {
        .bar #{&} {
          color: gray; } } } } }
.bar .foo { color: gray; }

@at-root in block form can remove a selector from specific directives.

Media:

@media print {
  @supports ( transform-origin: 5% 5% ) {
    .foo {
      @at-root without-media {
        .bar #{&} {
          color: gray; } } } } }
@supports ( transform-origin: 5% 5% ) { .bar .foo { color: gray; } }

Supports:

@media print {
  @supports ( transform-origin: 5% 5% ) {
    .foo {
      @at-root without-supports {
        .bar #{&} {
          color: gray; } } } } }
@media print { .bar .foo { color: gray; } }

Several directives removed:

@media print {
  @supports ( transform-origin: 5% 5% ) {
    .foo {
      @at-root without-supports, without-media {
        .bar #{&} {
          color: gray; } } } } }
.bar .foo { color: gray; }

Selector inheritance uses the resulting selector and directive context.

%outside-the-media-block { color: gray }
@media print {
  @supports ( transform-origin: 5% 5% ) {
    .foo {
      @at-root without-directives {
        .bar #{&} {
          // not an error because the directives were removed.
          @extend %outside-the-media-block;  } } } } }
.bar .foo { color: gray; }

In theory we can make without-xxx be agnostic of directive type, but this may have issues with prefixed directives. Or we can have a whitelist of directive names.

The cases around directive removal will solve a big problem with the limitations around using @extend within media blocks. Specifically, this allows you to extend a selector via a mixin for all media contexts instead of just within the current media block.

@nex3
Contributor
nex3 commented May 28, 2013

@at-root without-whatever is syntactically ambiguous with the @at-root <selector> syntax. If we're going to add flags like that, I think we'd need to find a better syntax for it. One possibility is using the !important flag syntax, as in @at-root !without-supports.

@chriseppstein
Member

@nex3 it is semantically ambiguous, but not syntactically.

@chriseppstein
Member

That is, it would be a semantic conflict if there was ever a html element added named without-xxx. But this is just a selector sequence at parse time.

@chriseppstein
Member

Another test case: Make sure interpolation works in @at-root.

@Hannes-III

Quite complex but cool! Why not use

@at-root (without:supports media){ ... }

Like the Syntax of the other at-rules?

@chriseppstein
Member

@Hannes-III I like this.

@chriseppstein
Member

Another test case: ensure that properties are disallowed within a block-style @at-root.

@nex3
Contributor
nex3 commented May 29, 2013

@nex3 it is semantically ambiguous, but not syntactically.

That is, it would be a semantic conflict if there was ever a html element added named without-xxx. But this is just a selector sequence at parse time.

Whatever you want to call it, I don't want to hijack some specific valid selector syntax to have a special meaning in this context.

@robwierzbowski
Contributor

I understand this idea is already pretty solid, so please take this semi-rhetorically.

With the block and flag additions to @at-root you're really adding a transform selector directive. Calling it something like @selector (at-root) (without: media) would keep the directive abstract and open for any additional selector transforming enhancements in future versions.

@robwierzbowski
Contributor

There are plenty of use cases for stripping @media and @supports without putting a selector at-root, but they're already possible with mixins. Being able to apply without on its own would simplify things though.

@chriseppstein
Member

There are plenty of use cases for stripping @media and @supports without putting a selector at-root

That's a good point. But I'm not sure it's common enough to have completely different behaviors. might as well have several directives at that point.

@robwierzbowski
Contributor

Either a single directive with multiple flags or multiple nestable directives works equally well for me, as long as the choice is consistant. I think it would be confusing if there's @at-root (without: media), @without-media, and @future-selector-transform (at-root).

I prefer single directives/functions/mixins with flags, but I know you're a big fan of breaking things out into descriptive single use pieces :).

@nex3
Contributor
nex3 commented May 31, 2013

I like the consistent syntax of @robwierzbowski's suggestion, but I don't like how verbose it makes the simple cases. What used to be @at-root #{&}-foo { ... } becomes @selector (without: parent) { #{&}-foo { ... } }, which is a lot more burdensome to read and write.

@robwierzbowski
Contributor

True. The all directive pattern would look like:

...selectors... {
  @at-root {
    @without-media {
      ...rules... }
    @future-selector-transformer {
      ...rules... } }

which I like too, now that I wrote it out.

@nex3
Contributor
nex3 commented Jun 10, 2013

I'm also not a big fan of adding a bunch of new directives for this. I'd like to find a way to do this with a single directive that can be configured to address the needs of all the use cases, but is easiest to use in the most common case.

One possibility would be to have @at-root default to removing all parents, be they directives or selectors, then be configurable using (with: ...) or (without: ...). This is very consistent, but it has the notable downside that @at-root #{&} foo means something different than & foo.

This suggests a second possibility: removing the parent selector but not any parent directives, while still being configurable. This fits the common case better, but is less consistent for more complex cases.

Finally, a much more radical possibility is to start thinking about bubblable directives like @media and @support as part of the parent selector. Thus something like .foo { @media screen { .bar { ... } } } would notionally have the selector .foo (@media screen) .bar. Perhaps that could even be writable by the user. Then we could use the first possibility I listed as the semantics for @at-root, and have @at-root #{&} foo resolve to .foo (@media screen) .bar foo, this giving it the same semantics as & foo. This would probably make selectors substantially more difficult to manipulate via SassScript, though, and may have other complicating factors as well.

@chriseppstein
Member

I'm opposed to making the default behavior of @at-root be to remove directive context. Such a behavior would ruin the primary use case of using @at-root to use & in SassScript. If that means we need two directives with different names, then that's what we need.

Regarding making directives part of &. I'm glad you're thinking outside the box, but I do not like this idea. I'd much rather introduce functions like current-media-query(), current-support-condition() and maybe some helper functions for working with the results. If we're going to do expose so much context, we should also expose current-property() as was mentioned in #727.

@nex3
Contributor
nex3 commented Jun 14, 2013

I'm opposed to making the default behavior of @at-root be to remove directive context. Such a behavior would ruin the primary use case of using @at-root to use & in SassScript. If that means we need two directives with different names, then that's what we need.

I agree that it's important that the default behavior of @at-root should support the primary use-case. However, I'd very much like to avoid additional directives. I'd much rather add options to at-root (the second possibility I listed above).

Regarding making directives part of &. I'm glad you're thinking outside the box, but I do not like this idea.

I'm by no means wedded to it, but I would like to understand more about why you dislike it.

I'd much rather introduce functions like current-media-query(), current-support-condition() and maybe some helper functions for working with the results.

I'm not a huge fan of adding a separate function for getting each different type of context. Regardless, this is something that will likely not happen for a while; without being necessary for @at-root, there's no pressing need to give access to parent directives.

If we're going to do expose so much context, we should also expose current-property() as was mentioned in #727.

This conclusion doesn't follow. Getting block-level parent context is very different than accessing a representation of a property that's actively being defined.

@chriseppstein
Member

However, I'd very much like to avoid additional directives.

I agree with you that the second option is the best so far, it was also what was already proposed 😄

I would like to understand more about why you dislike it.

Although it doesn't seem to have any backwards compatiblity issue at this time, it fundamentally changes (what I think is) the mental model of &. As we've agreed, & and @at-root #{&} should be essentially the same, but the change you're proposing would make .foo #{&} create illegal output unless, as you point out, we want to create a syntax for embedding directives within selectors which I'm very reticent to do but I asked on twitter about it to see what others think.

I'm not a huge fan of adding a separate function for getting each different type of context.

Obviously, alternative forms of the same concept (using a function to access non-selector context) are welcome and encouraged.

Getting block-level parent context is very different than accessing a representation of a property that's actively being defined.

I don't see a fundamental difference. Context is context. Please explain.

@nex3
Contributor
nex3 commented Jun 19, 2013

I don't see a fundamental difference. Context is context. Please explain.

The block-level context already exists when the SassScript is getting run; property values is being defined while the SassScript accessing it is run. Accessing block-level context preserves the invariant that $var: expression; property: $var works the same as property: expression; accessing property values does not. Block-level context is conceptually treated by Sass and CSS as a context, in that it provides meaning to the statements nested beneath it; property values are treated as values, not context.

@chriseppstein
Member

Accessing block-level context preserves the invariant

Gotcha. Seems worth preserving without a very compelling use case -- which I've not see to-date.

@JackCA
JackCA commented Jun 28, 2013

👍 on this feature. I have been looking for a solution to moving out of the current scope for a long time. Would be super helpful

@amacneil
amacneil commented Aug 2, 2013

👍 will be very helpful for writing media query mixins

@nex3 nex3 closed this Oct 11, 2013
@PabloMarch

Hi, Is there a way to access to the parent element to add a class? I used lot of times the parent to trigger css animations and would like to have something like this:

HTML:

<div class="base">
    <div class="item-1"></div>
    <div class="item-2"></div>
    <div class="item-3"></div>
</div>

SCSS:

.base {

    & > div {
        transition: all 1s;
        transform: scale(1);
    }

        .item-1 {
            properties...

            @go-out(1) #{&}.animated {
                transform: scale(0.70);
            }
        }

        .item-1 {
            properties...

            @go-out(1) #{&}.animated {
                transform: scale(0.70);
            }
        }

        .item-1 {
            properties...

            @go-out(1) #{&}.animated {
                transform: scale(0.70);
            }
        }

}

OUTPUT CSS:

.base > div {
    transition: all 1s;
    transform: scale(1);
}

.base .item-1 { properties... }

.base .item-2 { properties... }

.base .item-3 { properties... }

.base.animated .item-1 { transform: scale(0.70); }

.base.animated .item-2 { transform: scale(0.60); }

.base.animated .item-3 { transform: scale(0.50); }

and the parameter passed to @go-out(X) indicate the how many levels want to go out.

Thanks!

@nex3
Contributor
nex3 commented May 15, 2014

Not currently. In version 3.4, you'll be able to access the parent selector in SassScript using & and perform whatever operations you want on it.

@davestewart

Yeah, an @at-node(n) construct to traverse back up the selector tree would be great. I've needed that a few times.

@QuentinFchx QuentinFchx referenced this issue in csscomb/csscomb.js Feb 23, 2015
Open

Support for @at-root #353

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment