less.js icon indicating copy to clipboard operation
less.js copied to clipboard

Parent Selectors should have targets

Open scottrippey opened this issue 12 years ago • 123 comments

When creating a Parent Selector rule, everything before the & gets prepended to the outermost scope.

The feature could be greatly improved by allowing for "parent targeting", where you could apply the parent selector to a specific parent (in the case where you have deeply nested selectors).

The syntax for this feature is probably the biggest hindrance, so I would love to start a discussion on the possibilities.

scottrippey avatar Dec 13 '12 19:12 scottrippey

Example usage:

.main-content {
    .button {
        .button-icon {
            background-image: @button-icon;
            (.button):hover { background-image: @button-icon-hover; }
            (.button).focus { background-image: @button-icon-focus; }
            .ie8 (.main-content) { background-image: @button-icon-ie8; }
        }
    }
}

This may be a contrived example, but I have several real-world scenarios where such a syntax would be a life saver.

scottrippey avatar Dec 13 '12 19:12 scottrippey

There's a variety of solutions in LESS to handle this. You can assign variables and use those as part of selector chains (new in 1.3.1 I think). The & is not really a parent selector in the CSS4 sense of the term. It literally just appends the inherited selectors to wherever you place them.

matthew-dean avatar Dec 13 '12 19:12 matthew-dean

I usually just end up writing these code blocks like this:

.main-content {
    .ie8 & { background-image: @button-icon-ie8; }
    .button {
        &:hover { background-image: @button-icon-hover; }
        &.focus { background-image: @button-icon-focus; }
        .button-icon {
            background-image: @button-icon;
        }
    }
}

matthew-dean avatar Dec 13 '12 19:12 matthew-dean

I do the same thing -- I duplicate my nested blocks with the overrides. In a real-world scenario, this results in an unmanageable amount of duplication. FYI, in your example, the background-image needs to be applied to the .button-icon, so you'd have way more duplication.

scottrippey avatar Dec 13 '12 19:12 scottrippey

Can you explain "assign variables and use those as part of selector chains"?

scottrippey avatar Dec 13 '12 19:12 scottrippey

I'm probably wrong about that quote, so probably shouldn't try to explain it. That is, I haven't tried it personally.

So, it looks like what you want to do is modify a selector in the inherited chain. How would this work in this case?

.button {
.main-content {
    .button {
        .main-content {
            .button-icon {
                background-image: @button-icon;
                (.button):hover { background-image: @button-icon-hover; }
                (.button).focus { background-image: @button-icon-focus; }
                .ie8 (.main-content) { background-image: @button-icon-ie8; }
            }
        }
    }
}
}

It's messy CSS, but it's perfectly valid CSS.

matthew-dean avatar Dec 13 '12 19:12 matthew-dean

That's a good example ... if the "parent target" matches multiple items, we'd want to choose just one. Choosing the "closest" one is my first reaction. But I think this would be an edge case.

scottrippey avatar Dec 13 '12 21:12 scottrippey

So far, I've scratched down several attempts to invent a syntax that I like ... and the parenthesis so far is my favorite. It seems readable, and definitely catches your attention to highlight that this is no normal selector.

The only problem -- does this interfere with any existing CSS selector rules? Looking at the less parser, I see that a selector element can match /\([^()@]\)/ which means it's already being parsed correctly. I don't know if these parenthesis are valid.

I just realized that the parenthesis syntax looks almost exactly like a mixin definition ... with the exception of the class dot .. So maybe it's not a good idea. Feedback?

scottrippey avatar Dec 13 '12 22:12 scottrippey

Your use case sounds like what is discussed in #965 .. what do you think?

Bearing in mind & is quite powerful already and we are bringing in :extend() in 1.4.0, it seems to me if the only thing missing is being able to call a mixin that adds selectors to your selector chain, then if we decide to add that functionality it should be as simple as possible.. the more we add to selectors, the more complicate less gets to learn and understand.

lukeapage avatar Dec 14 '12 09:12 lukeapage

Yeah, I feel like, unfortunately, the logic gets a little messy, and like @agatronic, there may be other ways to fill in what you want in the future.

matthew-dean avatar Dec 14 '12 17:12 matthew-dean

I agree. This over-complication is a result of a very complicated LESS structure, and time would probably be better spent in simplifying the LESS code instead of introducing a very complicated syntax.

scottrippey avatar Dec 14 '12 19:12 scottrippey

I'm going to re-open this. We've seen variations on this idea (such as #1154), and I feel like there's a possibility of a solution.

That is, there are times when people have a logical stack of elements, but don't necessarily want to inherit the entire stack in their output.

Also, there are times when people want to inherit SOME of the stack, but not all of it.

I think we can all agree that we don't want messy syntax, but I'd like to see a variety of ideas. As we've explored here, targeting by name seems problematic. But we could also target by level:

.main-content {
    .button {
        .button-icon {
            background-image: @button-icon;
            &{2}:hover { background-image: @button-icon-hover; }
            &{2}.focus { background-image: @button-icon-focus; }
            .ie8 &{3} { background-image: @button-icon-ie8; }  // In this case, &{3} is the same as & because it goes to top
        }
    }
}

Basically, step "up" the inheritance tree to the level we want to inherit / append to the current selectors.

Or, something like, maybe "breaking" the inheritance and starting over, without having to move the class outside of the block. I dunno, like:

.grandparent {
  .parent {
    /.child {   // Ordered logically, but outputs cleaner CSS
      background: white;
    }
  }
  background: blue;
}

or, borrowing from the top example:

.grandparent {
  .parent {
    &{0}.child {   // inherit NONE of the parents
      background: white;
    }
  }
  background: blue;
}

Both outputting:

.grandparent {
  background: blue;
}
.child {
  background: white;
}

Thoughts?

matthew-dean avatar Feb 01 '13 20:02 matthew-dean

I like it. I prefer my suggestion of &1 or @Soviut &(1). I think probably that is my current winner.

lukeapage avatar Feb 04 '13 13:02 lukeapage

One thought.. Matthews solution doesn't allow you to have a mixin (with unknown inheritance) and just work within your current selectors (as in #1158). Is it too complicated to have &(2..n) .var &(1) ? or could you do & .var &(1) and the first & would be all the selectors not chosen already. alternatively should it default to putting the selector parts you don't use at the begining unless you use \ ?

lukeapage avatar Feb 04 '13 18:02 lukeapage

One question: why &(1) and not &{1}? The latter is similar to inline variable syntax, whereas parentheses feels kind of like a math operation. Or a mixin, and it's not similar to either. It's more like a reference.

How would it break mixins? Not understanding that.

matthew-dean avatar Feb 04 '13 19:02 matthew-dean

in a selector you have lots of pseudo classes using brackets and then you have variable substitution which borrows its syntax from the more verbose ~"@{var}" so for me normal brackets work better (especially if we should allow &(2..n)).. but having said that I don't have a strong preference between ( and {.. I'm more interested in the above.. how to step up 1 inheritance level verus breaking out of the current selector.

lukeapage avatar Feb 05 '13 08:02 lukeapage

I'm surprised no one has mentioned how to target specific (immediate) parents when more than one are present. Here's some CSS which I'd like to arrive at:

/* regular css */
.foo, .bar {
    margin: 0 auto;
    line-height: 1.2;
}
.bar {
    line-height: 2;  /* override */
}

Unfortunately, I actually have to write it like that in LESS syntax as well. It'd be nice to target .bar within the first block. Perhaps something like:

/* LESS css */
.foo, .bar {
    margin: 0 auto;
    line-height: 1.2;

    &(2) {
        line-height: 2;
    }
}

Then the ancestors could be accessed with additional & combinators (I like the "targeting by level" idea):

/* LESS css */
.mommy {
    .foo, .bar {
        margin: 0 auto;
        line-height: 1.2;

        &(2) {
            line-height: 2;
        }

        /* same results for &&(1) */
        &&.mommyClass {
            color: #000;
        }
    }
}

Smolations avatar Feb 26 '13 18:02 Smolations

@Smolations that came up in a different issue.

We thought that this might be done using guards on css rulsets combined with being able to test parent selectors in the guard condition. but good to bring it up.

lukeapage avatar Feb 26 '13 19:02 lukeapage

Ah, I see what you mean. I just didn't see that issue thread because it's subject didn't catch my eye (you wouldn't happen to know where I could find that discussion, would you? =] ).

I think that any time you can marry various functionality to a unified syntax, you contribute to the intuitiveness of the final product. Even if I hacked that functionality by using guards, and assuming this thread's idea is implemented in any of the suggested forms, achieving the desired result in my example would require using two separate syntax's. My suggestion tries to use the already-familiar syntax of the ampersand.

If my request (the ability to access a specific parent selector) isn't implemented in the future, I'd still like to see a solution to this thread's problem that uses the ampersand because it's already meaningful when accessing the LESS selector family tree. =]

Smolations avatar Feb 27 '13 01:02 Smolations

here https://github.com/cloudhead/less.js/issues/1174

for every feature we like to make sure it has real value-add.. if it makes it easier to not refactor your css to a better structure then it doesn't have value add. if it makes your less harder to understand, thats not good and if it makes less harder to learn, that isn't good either. So, I mostly support the simple functionality in this post (but not enough to make it high priority - unfortunately there are things more important), but I am really wary about how complex it should become. If we combine 2 existing features that have been asked for and as a side product allow you to do what you want to do, without making selectors more complicated, I think thats better unless your use-cases are general enough that special functionality should be added because they have so much value-add.

lets see if we can re-write your last example...

.mommy {
   .foo, .bar {
       margin: 0 auto;
       line-height: 1.2;
   }
   .bar {
       line-height: 2;
   }
   .mommyClass {
       color: #000;
   }
}

and lets say we want to not have to write the .bar selector twice..

.mommy {
   .foo {
       margin: 0 auto;
       line-height: 1.2;
   }
   .bar:extend(.mommy .foo) {
       line-height: 2;
   }
   .mommyClass {
       color: #000;
   }
}

With these examples, its quite easy to understand what the result css will be.. with yours, it isn't, without learning some more..

What we need to convince us of the more complicated parts of this proposal are real use cases that can't be done any other way.

lukeapage avatar Feb 27 '13 08:02 lukeapage

note - to implement the extend syntax options we will need to add a rule that the basics of the original request.. e.g. to escape the current nesting. We don't have to expose it, but we may as well. Do we really need specific parent targets?

For "escaping" I was thinking >| or something like that.. but yes, if we do need specific parent selectors &(0) to escape and the rest to target a specific nesting level seem to make some sense.

lukeapage avatar Feb 27 '13 08:02 lukeapage

Initially, we considered a lot of syntactic approaches for this feature in Sass but ultimately decided to do something much simpler because the number of use cases was very large:

We are exposing & to SassScript as a comma separated list of space separated values. Then all of the manipulation of the selectors can be done in "user space" by simply manipulating selectors with Sass functions.

For example:

.foo, .bar > a { first: nth(&, 1); second-combinator: nth(nth(&,2),2) }

would generate:

.foo, .bar > a { first: ".foo"; second-combinator: ">"}

We will have this feature in Sass 3.3. I suspect we'll also ship with a few standard selector manipulation functions and then wait to see what the community develops before standardizing any more.

chriseppstein avatar Apr 23 '13 22:04 chriseppstein

@chriseppstein, nice, I really like the flexibility of that approach. thank you

jonschlinkert avatar Apr 24 '13 04:04 jonschlinkert

@jonschlinkert :) & is a feature that Less adopted from Sass; I think the dev community is best served if they both continue to work similarly.

chriseppstein avatar Apr 24 '13 18:04 chriseppstein

@chriseppstein That's pretty cool, I like how & acts like a variable, on which you can perform funcitons. Makes this concept much easier to understand and read.

scottrippey avatar Apr 25 '13 00:04 scottrippey

so presumably you assign a selector to a variable and then if you need that in a selector, you use the variable.. I like that it doesn't further complicate the selector logic

lukeapage avatar Apr 25 '13 07:04 lukeapage

@a:~".c";
@{a}.b {
   & + & {
      .d(nth(&, 1));
   } 
}

take the above example.. We do this

  1. call .d
  2. evaluate @{a}.b
  3. work out & + &

This was partly justified because it used to be the case that calling mixins could pullute the current scope (something we no longer do).

Stages 1 and 2 are done in one step and 3 in another.

This is also done because until all the mixins have been called you cannot work out all the rulesets selectors.

I think to implement this you would have to have a concept of whether a ruleset was in a mixing-in state or not and at the point it leaves that state (or upon start of evaluation if its never been mixed in) we work out the selectors. I think that would work. Then we could get the paths at evaluation time.

The problem with that approach is extends.. These would have to be applied as you go along - again at the moment we process the AST for extends.

Another solution would be to continue to move towards the visitor model but either allow the visitors to run multiple times or else be cross dependent or something. hrmm.

I will have to think about this.

lukeapage avatar Apr 30 '13 22:04 lukeapage

How would it interact with "return mixins" https://github.com/cloudhead/less.js/issues/73 feature?

What would be result of this operation:

.some-class {
  #unlock-this {
    .mixin() {
      first: nth(&, 1); 
    }
  }

  .foo, .bar > a { 
    #unlock-this();
    .mixin();
  }
}

It could be either this:

.some-class .foo,
.some-class .bar > a {
  first: #unlock-this;
}

or this:

.some-class .foo,
.some-class .bar > a {
  first: .some-class .foo;
}

SomMeri avatar May 01 '13 13:05 SomMeri

I would expect for it to be useful, for & to equal the final selector, as it does when used as part of selectors.

lukeapage avatar May 01 '13 14:05 lukeapage

It looks like there's some powerful stuff here for referencing the inherited selector. Interesting.

I still also like the idea of a simple escape character, to just turn off block inheritance without much fanfare. CSS Crush uses a caret ^. Seems simple. How about it?

.this {
  .is {
    .deeply {
      .nested {
        ^.related-but-simpler-output {
          property: awesome;
        }
      }
    }
  }
}

// Outputs

.related-but-simpler-output {
  property: awesome;
}

The nth() stuff is cool too (for "choosing" the inherited selector), which I don't think would conflict with supporting this.

matthew-dean avatar May 26 '13 17:05 matthew-dean