sass / sass

Sass makes CSS fun!
https://sass-lang.com
MIT License
15.06k stars 2.15k forks source link

Support "selector&" #1425

Open maranomynet opened 10 years ago

maranomynet commented 10 years ago
.bar {
    foo& { 
        color: baz;
    }
}

Causes an error. (See: http://codepen.io/anon/pen/GIufH)

This is surprising (since .bar { foo & { ... } } works like a charm), and this error prevents several useful selector prefixing patterns.

lolmaus commented 10 years ago

Use interpolation, @maranomynet:

.bar {
    foo#{&} { 
        color: baz;
    }
}

Demo: http://sassmeister.com/gist/da2394d0d28b0fe95d59

Works in Sass 3.4.

maranomynet commented 10 years ago

Interesting hack. It suppresses the error but...

.bar foo.bar {  color: baz;  }

...is not the desired CSS.

It should be:

foo.bar {  color: baz;  }
lolmaus commented 10 years ago

It's not a hack, it's the way meant to be done.

...is not the desired CSS.

In this case, use the @at-root directive:

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

Demo: http://sassmeister.com/gist/8bd827341243d66e61d5

maranomynet commented 10 years ago

Still, the arbitrary discrepancy between this working smoothly:

.bar {
    foo & {  color: baz;  }
}

and this throwing an error:

.bar {
    foo& {  color: baz;  }
}

seems like a bug. (Breaking the principle of least astonishment.)

lolmaus commented 10 years ago

Using the & parent selector without interpolation was a temporary feature in Sass 3.3, it is now deprecated.

Sass follows strict logic for how the parent selector works. It is indeed not very intuitive in more complex cases, but there's no sense in flowing against the current.

maranomynet commented 10 years ago

Neither intuitive nor explained in http://sass-lang.com/documentation/file.SASS_REFERENCE.html#parent-selector

lolmaus commented 10 years ago

I might be wrong about the deprecation status.

Unfortunately, @chriseppstein did not explain the differences in behavior between the normal and interpolated parent selector. I believe it might be as follows:

  1. If & appears in the selector, the previous selector in hierarchy is reconstructed.
  2. If & does not appear in the selector, the current selector is appended to the previous selector.
  3. Interpolated & is not treated as an appearance.
  4. The @at-root directive removes the previous selector, unless it's explicitly inserted via &.
maranomynet commented 10 years ago

if this is possible:

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

wouldn't it be possible (and reasonable!) for SASS to do this automatically for you, whenever it encounters a pattern like this:

.bar {
    foo& { color: baz; }
}

??

nex3 commented 10 years ago

Using the & parent selector without interpolation was a temporary feature in Sass 3.3, it is now deprecated.

This is inaccurate. Using & with a suffix is still supported. Using it with a prefix (as here), however, has never been supported.

if this is possible:

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

wouldn't it be possible (and reasonable!) for SASS to do this automatically for you, whenever it encounters a pattern like this:

.bar {
    foo& { color: baz; }
}

??

The reason we don't allow this is that foo& can often produce unclear or simply invalid selectors, depending on the value of &. In general, & may only be used anywhere an element selector can be used, since it might be replaced with an element selector. We made an exception for &-suffix because that was a very common use-case that actually did end up working how people expected most of the time, but I don't think the same is true for &.

Using #{} is a better solution here because it makes it explicit that you're doing string-based selector substitution rather than using Sass's selector resolution. I'd suggest you use the new selector functions, though:

.bar {
  @at-root #{selector-append(foo, &)} { color: baz; }
}

This will play more nicely with parent selectors containing commas.

lolmaus commented 10 years ago

@nex3, thank you for clarifications.

But don't you agree that

.bar {
  @at-root #{selector-append(foo, &)} { color: baz; }
}

Is way too much typing, compared to LESS'

.bar {
 foo& { color: baz; }
}

There should be a simple way to write that. In projects using this convention, people type it like 100 times every day.

maranomynet commented 10 years ago

There should be a simple way to write that...

+1

nex3 commented 10 years ago

I suppose it doesn't produce many more issues than allowing &foo. I'll mark it as planned.

lolmaus commented 10 years ago

Thank you for changing your mind, @nex3. People keep stumbling upon this: https://github.com/sass/sass/issues/1436

maranomynet commented 10 years ago

Yay!

adamreisnz commented 9 years ago

Any idea when this will be implemented? I'm banging my head against this as well. Converted all my LESS files to SCSS because I was told SASS was better than LESS, but was very disappointed to find I couldn't do button& as a selector to get for example button.something.

maranomynet commented 9 years ago

It was issues like this one (and quite a few other similar ones) that pushed me away from SASS over to Stylus. It is incredibly expressive and user-friendly.

saper commented 8 years ago

As it actually turns out the libsass https://github.com/sass/libsass/tree/13ae62def291c4934ad04d0d527e84b4021d60a3 actually accepts this syntax:

https://github.com/sass/libsass/issues/1822

arkonan commented 8 years ago

+1

The selector& syntax is really useful for UI widget architectures where classes affecting display of the a widget are set on the outermost DOM element of the widget.

For example let's say I have a list item widget where multiple child elements of the li have some of their styles overridden depending on whether or not a particular icon is disabled. Rather than modifying each of the child elements I just set a no_icon class on the li.

So if my li element has the class widget_list_item then the selector& syntax lets me do the following:

.widget_list_item {
    a {
        padding-right: 24px; /* icon width */
        .no_icon& {
            padding-right: 0;
        }

        .title {
            margin-right: 0.375rem; /* spacing between title and icon */
            .no_icon& {
                margin-right: 0;
            }
        }
    }
}

In this simple example flipping it to use &.no_icon { ... } wouldn't make it harder to read. However once the styles and selectors for different display settings start to accumulate the selector& syntax keeps related styles together, helping with readability and maintainability.

The @at-root syntax would also work here but it's so verbose that I find it makes the SCSS harder to read.

thany commented 6 years ago

Please keep in mind that @at-root doesn't always produce the desirable result.

Consider this:

.wrapper {
    .field {
        input.& { ... }
        select.& { ... }
    }
}

The suggested workaround becomes:

.wrapper {
    .field {
        @at-root input#{&} { ... }
        @at-root select#{&} { ... }
    }
}

Which produces:

input.wrapper .field { ... }
select.wrapper .field { ... }

While I would want it to be:

.wrapper input.field { ... }
.wrapper select.field { ... }

I'm sure it's possible to juggle things around to make Sass output the desired css, but with reusable includes/mixins it becomes unneccesarily hard.

This coincides with the desire for a true parent selector, in addition to the not-parent-but-something-like-context selector &.

nex3 commented 6 years ago

That's a good use-case for selector functions:

.wrapper .field {
  @at-root #{selector-unify(&, "input")} {/* ... */}
  @at-root #{selector-unify(&, "select")} {/* ... */}
}

you can even make that into a mixin if you want:

@mixin unify-parent($child) {
  @at-root #{selector-unify(&, $child)} {@content}
}

.wrapper .field {
  @include unify-parent("input") {/* ... */}
  @include unify-parent("select") {/* ... */}
}
thany commented 6 years ago

Okay. From the documentation, I would have never known to use selector-unify for this. It's unclear to me what that actually does under the bonnet. And trying out your example (it works) I can't figure out how it sort of knows where to put input and select, respectectively.

Still, it's quite convoluted to require @at-root, interpolation, and a function that just feels like black magic.

On top of that, the fact that there is a verbose way of doing things, doesn't mean that doesn't need to be a clear and easier to understand way of doing the same thing.

nex3 commented 6 years ago

Okay. From the documentation, I would have never known to use selector-unify for this. It's unclear to me what that actually does under the bonnet. And trying out your example (it works) I can't figure out how it sort of knows where to put input and select, respectectively.

selector-unify(), like all Sass functions, is documented on the functions page. As the docs say, it returns "a single selector that matches only elements matched by both input selectors". If you have ideas for clarifying that documentation, you're welcome to submit a pull request.

Still, it's quite convoluted to require @at-root, interpolation, and a function that just feels like black magic.

On top of that, the fact that there is a verbose way of doing things, doesn't mean that doesn't need to be a clear and easier to understand way of doing the same thing.

You're trying to do is pretty advanced selector manipulation, so I don't think it's too surprising that you need to rely on some more advanced features. & is extremely simple: it textually includes the parent selector. That's easy to explain and easy to reason about. There's a lot of value to that simplicity, especially for users who are new to Sass.

But we recognize that it's not always sufficient for every use-case, so we provide a suite of selector functions and the ability to refer to the parent selector from SassScript. These advanced features make it possible for users to implement more complex behaviors on their own and wrap them up into reusable mixins like unify-parent().

Your use-case may not be the same as the next user's, though. Adding easy shortcuts for every single use-case ultimately fights against clarity; you end up with so many shortcuts that users (especially new users) can't keep track of what any given shortcut means. So it's our design philosophy to provide a small set of tools (like @at-root and selector functions) that can be used by users to build a large set of behavior.

thany commented 6 years ago

selector-unify(), like all Sass functions, is documented on the functions page. As the docs say, it returns "a single selector that matches only elements matched by both input selectors". If you have ideas for clarifying that documentation, you're welcome to submit a pull request.

The fact that it's there, doesn't mean folks will remember to use it, even if they understand the description. I'm not sure how else to describe it - technically it's correct. Perhaps an example or two will help.

As for the rest, you're right of course. But you also seem to contradict yourself. There is indeed a lot of value to the simplicity of &, but folks seem to want the additional simplicity of being able to do selector&. The dilemma is clear though. Keep expanding the capability of &, or stick with a slightly verbose syntax for things it's not capable of?

One of these choices will not appeal to novice or casual users, which there are a lot of. Advanced users aren't afraid of verbose syntax I'd say, but a team of frontend devs doesn't usually consist of only senior members.

arkonan commented 6 years ago

Personally I think that supporting selector& would be easier for users to understand. Currently the situation is like this:

& selector { }  // works
selector & { }  // works
&selector { }   // works
selector& { }   // doesn't work

I do agree that if Sass chooses to support selector& that it should have a behavior consistent with &selector. Having selector& behave like selector-unify() when &selector behaves like @at-root would be even more surprising than it just not working.

nex3 commented 6 years ago

To be clear, this issue is marked as "Planned" because we plan to add support for selector&. It will work the same way as &selector does.

yufengwang commented 6 years ago

Have we supported selector& yet ?

slavafomin commented 5 years ago

I even wrote a small post about this issue: https://medium.com/web-developers-path/using-parent-selector-prefixes-in-sass-5adf22ff7078

It's funny how this is being discussed for a five years already = )

oles commented 5 years ago

As a previous user of less and stylus, I'd really like to see this implemented!

I'd love to help getting this done as well. Some pointers would be highly appreciated.

Is this a proposal? I cannot find anything resembling this in either https://github.com/sass/sass/tree/master/proposal nor https://github.com/sass/sass/tree/master/accepted

If not - how to go about issues marked as planned?

Also - what is the connection between the milestones that have appeared in this issue (3.4, 4.0) and this repository?

nex3 commented 5 years ago

The full process for adding a new feature is described in CONTRIBUTING.md. Once you've read that, let me know if you have any more questions, or just feel free to open a pull request with a formal proposal.

oles commented 5 years ago

Perfect, thank you @nex3!

Had a quick glace at it now, and it seems to cover all I need. I'll take a shot at it next session!

oles commented 5 years ago

There we go! I've most likely missed something and/or made some mistake, but the first draft is out: https://github.com/sass/sass/pull/2722

All feedback more than welcome - my first time doing anything like this - thank you!

langlan commented 3 years ago

Encountered the same problem a few years ago and taking a couple of hours to re-understand it. Like others said, the solution for now is just counterintuitive or verbose. I feel so sad that this issue is still open since it is 2021.

AlAminKh03 commented 1 year ago

I'm just learning Sass, So as a newbie, I might be wrong but it's clear to me that it's a syntax error. when you want to use &(parent selector) as a suffix you have to add white space before it. Just like they showed in their documentation **.alert { // The parent selector can be used to add pseudo-classes to the outer // selector. &:hover { font-weight: bold; }

// It can also be used to style the outer selector in a certain context, such // as a body set to use a right-to-left language. [dir=rtl] & { margin-left: 0; margin-right: 10px; } }** see in the first nested they used it as prefix that's why they didn't add any whitespace but in the second nested element they used it as a suffix that's why they used extra whitespace before that. It just seems to me the correct way.

nex3 commented 1 year ago

An important update here: the CSS nesting specification is now using foo& to mean "add the element selector foo to the parent selector", so we'll need to match those semantics if/when we do add support for this format rather than adding a prefix to an existing selector.

CodeSmith32 commented 10 months ago

I'm hoping this could be implemented soon. I wrote my own reduced impl. of something like Sass, and, as &-postfixing was automatically available, I've extensively used tricks like the following:

/* simplify selectors with long class prefixes */
/* -> .long-class-name-suffix1.long-class-name-suffix2 {...} */
.long-class-name {
    &-suffix1&-suffix2 {
        property: value;
    }
}

/* increase specificity */
/* .some-class.some-class.some-class {...} */
.some-class {
    &&& {
        property: value;
    }
}

Sadly, because of this issue, neither of these tricks are supported with Sass currently.

mephistia commented 10 months ago

According to Can I use..., the :is() pseudo-class is valid for ~ 95% global users as of November 2023. I believe something like this could work instead:

.class {
  &:is(div) {
    color: black;
  }

  h2:is(&__child) {
    color: white;
  }
}
CodeSmith32 commented 10 months ago

@mephistia, Thanks for the input. That does work in both my cases, assuming support for :is,

/* simplify selectors with long class prefixes */
/* -> .long-class-name-suffix1.long-class-name-suffix2 {...} */
.long-class-name {
    &-suffix1:is(&-suffix2) {
        property: value;
    }
}

/* increase specificity */
/* .some-class.some-class.some-class {...} */
.some-class {
    :is(&):is(&):is(&) { // first :is isn't needed, but maintains the idiom, and c/p is easier
        property: value;
    }
}

It still would be nice if it wasn't necessary to use :is... &:is(&):is(&) isn't quite as succinct as &&&, but at least it is still way better than .very-long-classname.very-long-classname.very-long-classname.

cduivis commented 4 months ago

I also walked into this problem today, as I'm converting from Less to Sass. And to give you an example of 1 of the 59 occurrences we have in our style base: image

I see somethings have already been merged, but the issue is still open after 10 years. Is there a timeline or an expected version of npm package in which this will become available?

nex3 commented 4 months ago

@cduivis That pattern will never work in Sass, because it's now explicitly against the official CSS semantics for parent selectors. You'll need to write:

.tw-teaser-card {
    &--actionable > &__content a[href]#{&}__action {
        // ...
    }
}
cduivis commented 4 months ago

@nex3 Hey thanks for the reply, that work around isn't too bad.

Btw what do you mean with "official CSS semantics for parent selectors"? I haven't seen any proposal in the W3C CSS documentation yet for a parent selector?

I do want to mention that when using the BEM methodology in CSS, that these kind of selector combinations become common place, especially when dealing with more complex / fluid styles.

And I have to confess that somehow it feels odd that we need a special syntax to achieve these kind of things in Sass, while it feels like a natural and obvious code that you would want to write as a styler.

nex3 commented 4 months ago

Btw what do you mean with "official CSS semantics for parent selectors"? I haven't seen any proposal in the W3C CSS documentation yet for a parent selector?

https://drafts.csswg.org/css-nesting-1/#nest-selector