Build a Repetitive Selector Within a Less Loop

Build a repetitive selector within a Less loop

I would go about it somehow in this manner:

.generateClasses (@index, @n, @in:"") when (@index > 0) {
@concatenate: "@{in} .repeatedClass";
@selector: ~".staticClass @{concatenate} > .finalStaticClass";
@{selector}{ height: unit(@n,px) };
.generateClasses((@index - 1), (unit(@n) + 10), @concatenate);
}
.generateClasses(0, @n, @in){};

.generateClasses(4, 10px);

Where you pass on to the next loop the concatenated generated classes and each time add another class. The @index is the counter for the loop, and @n is the value that you want to increase.

CSS output:

.staticClass  .repeatedClass > .finalStaticClass {
height: 10px;
}
.staticClass .repeatedClass .repeatedClass > .finalStaticClass {
height: 20px;
}
.staticClass .repeatedClass .repeatedClass .repeatedClass > .finalStaticClass {
height: 30px;
}
.staticClass .repeatedClass .repeatedClass .repeatedClass .repeatedClass > .finalStaticClass {
height: 40px;
}

Edit - for older versions of Less:

in Less <= 1.3.3, you need to include the individual concatenating loops in a separate role (it is called .test in the example below), that confines the variable. Then you can loop through this, doing something along these lines:

.generateClasses (@index, @n, @in:"") when (@index > 0) {
@concatenate: "@{in} .repeatedClass";
@selector: ~".staticClass @{concatenate} > .finalStaticClass";
.generateClasses((@index - 1), (unit(@n) + 10), @concatenate);
}
.generateClasses(0, @n, @in){};

.test(@i, @ni){
.generateClasses(@i,@ni);
@{selector} {
height: @ni;
}
}

.printClasses(@i:1,@ni:10px) when (@i > 0) {
.test(@i,@ni*@i);
.printClasses(@i - 1,@ni);
}

.printClasses(4);

output CSS will now be:

.staticClass  .repeatedClass .repeatedClass .repeatedClass .repeatedClass > .finalStaticClass {
height: 40px;
}
.staticClass .repeatedClass .repeatedClass .repeatedClass > .finalStaticClass {
height: 30px;
}
.staticClass .repeatedClass .repeatedClass > .finalStaticClass {
height: 20px;
}
.staticClass .repeatedClass > .finalStaticClass {
height: 10px;
}

if you just need to generate a selector once at a time, you can skip the second loop and just call the .test() mixin wherever you need it.

How to generate CSS with loop in less

All, I found a way to output css in loop. pleae review it .thanks.

@iterations: 100;

// helper class, will never show up in resulting css
// will be called as long the index is above 0
.loopingClass (@index) when (@index > 0) {

// create the actual css selector, example will result in
// .myclass_30, .myclass_28, .... , .myclass_1
(~".span@{index}") {
// your resulting css
width: percentage((@index - 1) *0.01);
}

// next iteration
.loopingClass(@index - 1);
}

// end the loop when index is 0
.loopingClass (0) {}

// "call" the loopingClass the first time with highest value
.loopingClass (@iterations);

Build list of selectors with LESS

As already mentioned your attempt is almost there, it does not work because of variable visibility rules.
Notice that each .selector-list iteration defines new @selector-list variable which have higher precedence in the current scope but does not override @selector-list variables for the outer scope (i.e. scopes of the previous .selector-list iterations and above). So when you use @selector-list after the initial .selector-list call you get the value set in the "highest"
.selector-list iteration (i.e. the first one with @i = 1).

To "return" a value from the last iteration of a recursive loop you need to define a variable with this value only within that last iteration. Usually the simplest way to do this is to provide a "terminal" mixin (i.e. a specialization for the last call of the recursive mixin). In fact you would need such terminal anyway to handle the final comma of the list. E.g.:

#1

.selector-list(@parent, @children, @i: 1, @list: "") when (@i < length(@children)) {
@child: extract(@children, @i);
.selector-list(@parent, @children, (@i + 1), "@{list} @{parent} @{child},");
}

.selector-list(@parent, @children, @i, @list) when (@i = length(@children)) {
@child: extract(@children, @i);
@selector-list: e("@{list} @{parent} @{child}");
}

// usage:

@text-elements: p, ul, ol, table;

.selector-list("body.single .entry-content", @text-elements);
@{selector-list} {
line-height: 1.8;
}

-

#2 Same as above just slightly "optimized":

.selector-list(@parent, @children, @i: length(@children), @list...) when (@i > 1) {
.selector-list(@parent, @children, (@i - 1),
e(", @{parent}") extract(@children, @i) @list);
}

.selector-list(@parent, @children, 1, @list) {
@selector-list: e(@parent) extract(@children, 1) @list;
}

// usage:

@text-elements: p, ul, ol, table;

.selector-list("body.single .entry-content", @text-elements);
@{selector-list} {
line-height: 1.9;
}

-

#3 Speaking of the use-case in general, a "string-based selector manipulation" is not always a good idea in context of Less. The main problem is that Less does not treat such strings as "native" selectors and most of advanced Less features won't work with them (e.g. Less won't recognize ,, & and similar elements there so such rules can't be nested, extend also can't see such selectors etc. etc.).
An alternative, more "Less-friendly" approach is to define such list as a mixin rather than a variable, e.g.:

.text-elements(@-) {p, ul, ol, table {@-();}}

body.single .entry-content {
.text-elements({
line-height: 1.8;
});
}

and (when you also need to reuse body.single .entry-content /.text-elements/):

.text-elements(@-) {
p, ul, ol, table
{@-();}}

.selector-list(@-) {
body.single .entry-content {
.text-elements(@-);
}
}

.selector-list({
line-height: 1.9;
});

etc.

-

P.S. Also, speaking in even more general, don't miss that in Less a media query may be put into selector ruleset, so depending on a use-case it's also often more easy to write a list of selectors once and set media depended styles inside it (i.e. doing it in opposite to the standard CSS method where you have to repeat same selector(s) for each media query).

How do I abstract repetitive selector from this Less code?

There were a couple of minor problems with the code that you were trying but on the whole you were on the correct path. Making the following corrections would solve the issue.

  1. When trying to concatenate variable values to a string and get the concatenated string as output, you should put the whole thing within quotes like shown below. Note that when you put the whole thing within quotes, Less would print the output value also with quotes which we don't need because we would be using it in a selector. Hence, we should use the ~() or the e() functions to strip the quotes from output.

    @item:~".span-@{outer-index}-of-@{max-grid}";
  2. Secondly, when using a variable in selector interpolation to form a selector dynamically, the variable should be of the form @{variable-name} (within curly braces) and so for your case it should be modified to be like below:

    @{item} .row:first-of-type @{item} {
    color: blue;
    }

Finally, the approach suggested earlier by Eric would/should also work if we make the same change as mentioned in Point 2 above (due to selector interpolation being used). However, when this whole selector is nested within another parent, the & (parent selector) would always mean the whole parent upto the top-most level and hence it might not work as expected for all cases.

For example, consider a nesting scenario like below:

.foo{
@{item} {
& .row:first-of-type & {foo:bar;}
}
}

When compiled, the above would produce the following CSS. Here we can see how the .foo parent comes both at the start and the end.

.foo .span-1-of-3 .row:first-of-type .foo .span-1-of-3 {
foo: bar;
}

LESS Repeat selector declaration inside a mixin

Updated for LESS 1.5

This code produces the same effect more efficiently in the later versions of LESS, using the newer extract() and length() functions available in LESS 1.5+. Output will be the same as the original example.

.i(@file:'file.png', @types) {

//find length to make the stop point
@stopIndex: length(@types);

//set up our LESS loop (recursive)
.loopTypes (@index) when (@index =< @stopIndex) {
@class: extract(@types,@index);
//print the CSS
&.@{class} {
td:first-child {
background-image: url('../img/@{file}');
}
}

// next iteration
.loopTypes(@index + 1);
}

// "call" the loopingClass the first time getting first item
.loopTypes (1);
}

.myClass {
.i('code.png'; asp, php, rb, py;);
}

With Loops and Inline-Javascript in LESS 1.3.3

This took a few hours to come up with (no, I didn't have a bunch of free time to work on it, I'm just hopelessly addicted...). One of the parts that took the longest was figuring out why my @stopIndex was not being seen as a number by LESS when I was returning the .length of the array, and throwing a type error. I finally discovered I need to explicitly tell it to see it as a number using the unit() function of LESS.

The solution utilizes general concepts from these sources:

  1. The LESS looping
  2. The Javascript functions in LESS

LESS

.i(@file:'file.png', @type) {
//find length to make the stop point
@stopIndex: unit(`(function(){ return @{type}.split(",").length})()`);
//need to get the first item in @type
@firstClass: ~`(function(){
var clsArray = @{type}.replace(/\s+/g, '').split(",");
return clsArray[0];
})()`;

//set up our LESS loop (recursive)
.loopTypes (@index, @captureClass) when (@index < @stopIndex) {
@nextClass: ~`(function(){
var clsArray = @{type}.replace(/\s+/g, '').split(",");
//don't let it try to access past array length
if(@{index} < (@{stopIndex} - 1)) {
return clsArray[@{index} + 1];
}
else { return '' }
})()`;

//print the CSS
&.@{captureClass} {
td:first-child {
background-image: url('../img/@{file}');
}
}

// next iteration
.loopTypes(@index + 1, @nextClass);
}

// define guard expressoin to end the loop when past length
.loopTypes (@stopIndex, @captureClass) {}

// "call" the loopingClass the first time getting first item
.loopTypes (0, @firstClass);
}

.myClass {
.i('code.png', 'asp, php, rb, py');
}

CSS Output

.myClass.asp td:first-child {
background-image: url('../img/code.png');
}
.myClass.php td:first-child {
background-image: url('../img/code.png');
}
.myClass.rb td:first-child {
background-image: url('../img/code.png');
}
.myClass.py td:first-child {
background-image: url('../img/code.png');
}

Generating selectors with Stylus loop

OK. So, this question is unique to the duplicate I mentioned - mainly because there are 2 arrays used in the loop. You may want to rename the question for future searches.

First off, it took me a long time to realise I needed to take error messages seriously. In your case - it's telling you there are brackets - and there shouldn't be - so they are unexpected. Remove them. Stylus syntax is mostly just like SCSS - all you have to do is type less things - and ensure that your indents are perfect. The indents replace the brackets in clarifying the rules' beginning and end. Remove ALL brackets - and semicolons. *(Side note - use $var for variables / they are likely to be required later - also - : used to be an option - they are also going to be required)

Second, I'm guessing that this for loop is like an each loop in Javascript, so you can get currentvalue, currentIndex, fullArray parameters out with a comma separated list. (I'm not 100% on that)

for fillColor, currentIndex in $fillColorArray

This would allow you to access the color and its index as these placeholders.

Here is alive example: https://codepen.io/sheriffderek/pen/64c6791116c3a180cb196610f9962f17/ - you can choose to view the compiled CSS in the stylus pane's little arrow icon.


markup

<ul class="chart-list one">
<li class="chart">
<span>Chart 1</span>
</li>
<li class="chart">
<span>Chart 2</span>
</li>
<li class="chart">
<span>Chart 3</span>
</li>
<li class="chart">
<span>Chart 4</span>
</li>
</ul>

...

You can do this a few ways - depending on the application. Here is an example with 2 loops - and another with a single loop.


stylus

$fillColorArray = (#f06 pink)
$textColorArray = (white #f06)

remove-list-styles()
list-style: none
margin: 0
padding: 0

.chart-list
remove-list-styles()
margin-bottom: 2rem
background: lightgray
.chart
padding: 1rem

.chart-list.one
//
.chart
//
for fillColor, currentIndex in $fillColorArray
&:nth-of-type({currentIndex + 1})
background: fillColor
for textColor, currentIndex in $textColorArray
&:nth-of-type({currentIndex + 1})
span
color: textColor

.chart-list.two
//
.chart
//
for fillColor, currentIndex in $fillColorArray
&:nth-of-type({currentIndex + 1})
background: fillColor
span
color: $textColorArray[currentIndex]

// &:nth-of-type( 2n + {currentIndex + 1})
// if you want it to repeat at those intervals
.chart-list.three
//
.chart
//
for fillColor, currentIndex in $fillColorArray
&:nth-of-type( 2n + {currentIndex + 1})
background: fillColor
span
color: $textColorArray[currentIndex]

Unable to execute a Less CSS loop properly

You've got your loop's guard conditions wrong. The guard condition states that loop will be executed only when input (@i) is greater than 15 but the value that is passed as input (@gap2) is only 10 and hence the loop never gets executed.

For the output that you are expecting, change the guard condition like in the below snippet. Now, the guard is @i > 0 and so the loop will get executed but the selector interpolation in the 2nd mixin uses the @j variable (which is @i + @gap1). Since we are adding @gap1 to the loop's index, the no. value appended to the selector will be greater than 10 for the second loop.

@temp0-9: #1976d2;
@temp10-20: #00bcd4;
@gap1: 10;
@gap2: 10;

.first (@i) when (@i > 0) {
span.temp-@{i} {
display:block;
background: @temp0-9;
}
.first(@i - 1);
}
.first(@gap1);

.second (@i) when (@i > 0) {
@j: @i + @gap1;
span.temp-@{j} {
display:block;
background: @temp10-20;
}
.second(@i - 1);
}
.second(@gap2);

Demo @ Less2CSS.org


If you have multiple such gaps, then writing a single loop (with complex logic) would be better than writing multiple loop mixins. Below is a sample:

@gaps: 46, 19, 3, 4, 4, 14; /* the gap array */
@temps: red, crimson, orange, gold, yellow, green; /* the temps corresponding to each gap */

.gaps-loop(@i, @prevgap) when (@i > 0){
@gap: extract(@gaps, @i); /* extract each gap one by one based on loop index */
@temp: extract(@temps, @i); /* extract the temp corresponding to each gap */
.span-gen-loop(@j) when (@j < @gap){
/* loop to generate spans - executed as many times as @gap */
@k: @j + @prevgap; /* add current index to previous gaps - this generates a running number from 0-90 */
span.temp-@{k}{
display:block;
background: @temp;
}
.span-gen-loop(@j + 1);
}
.span-gen-loop(0);
.gaps-loop(@i - 1, @prevgap + @gap); /* send current gap + previous gap(s) */
}
.gaps-loop(length(@gaps), 0); /* loop as many times as there are gaps */

How to select a range of elements in repeated pattern

This is a commonly-asked question, but I wanted to point out that the reason :nth-child(n+4):nth-child(-n+6) only matches one specific range of elements is that it only provides a single start point (n+4) and a single end point (-n+6). The only elements that can be greater than or equal to 4 and less than or equal to 6 are 4, 5 and 6, so it's impossible to match elements outside of this range using the same selector. Adding more :nth-child() pseudos will only narrow down the matches.

The solution is to think of this in terms of columns, assuming there will always be exactly 3 columns (elements) per row. You have three columns, so you will need three separate :nth-child() pseudos. Elements 4 and 10 from the first column are 6 elements apart, so the argument to all of the :nth-child() pseudos needs to start with 6n.

The +b portion in the An+B expression can either be +4, +5 and +6, or 0, -1 and -2 — they will both match the same set of elements:

  • li:nth-child(6n+4), li:nth-child(6n+5), li:nth-child(6n+6)
  • li:nth-child(6n), li:nth-child(6n-1), li:nth-child(6n-2)

You cannot do this with a single :nth-child() pseudo-class, or a single compound selector consisting of any combination of :nth-child() pseudos, because the An+B notation simply doesn't allow such expressions to be constructed that match elements in ranges as described.

Less CSS - cut down repetition

You could use loops and array list to reduce the repetition in the code. The below is a sample snippet on how to achieve reduction.Refer inline comments for explanation on what the code is doing.

Note: I have made the actual padding generation mixin as a separate one which takes the sides as an argument because you can re-use that mixin to generate padding for multiple sides (by passing the sides and gutter as arguments) without generating media-queries for them.

@gutters: 4px, 6px, 8px, 10px; // the gutter sizes corresponding to each screen size
@media-sizes: xs, sm, lg, md; // possible screen sizes
@media-conditions: ~"(min-width: 100px)", ~"(min-width: 150px)", ~"(min-width: 200px)", ~"(min-width: 250px)"; // media condition for each screen size

.media-generator(){
.loop-sizes(length(@media-sizes)); // loop through all screen sizes
.loop-sizes(@screenIndex) when (@screenIndex > 0) {
& when (extract(@media-sizes, @screenIndex) = xs){ // since we need xs as default
.padding-per-side(extract(@gutters, 1); left; right; bottom; top);
}
& when not (extract(@media-sizes, @screenIndex) = xs){ // when screen size is not xs
@condition: extract(@media-conditions, @screenIndex); // extract media condition corresponding to screen type
@media @condition{
.padding-per-side(extract(@gutters, @screenIndex); left; right; bottom; top); // call the mixin to generate padding for all sides
}
}
.loop-sizes(@screenIndex - 1);
}
}
.padding-per-side(@gutter; @sides...){
.loop-sides(length(@sides));
.loop-sides(@index) when (@index > 0){
@side: extract(@sides, @index);
&.@{side}{
padding-@{side}: @gutter;
}
.loop-sides(@index - 1);
}
}
.padding{
.media-generator(); // generate padding for all sides and screens like in question
}

#demo{ // extra :)
.padding-per-side(10px; left;right); // generates 10px padding for left and right
}

The below is an enhanced version of the above which allows us to generate the padding with media queries only for some sides. The difference between the below snippet and the above one is that here you can generate padding for specific sides alone along with their media-query version.

@gutters: 4px, 6px, 8px, 10px;
@media-sizes: xs, sm, lg, md;
@media-conditions: ~"(min-width: 100px)", ~"(min-width: 150px)", ~"(min-width: 200px)", ~"(min-width: 250px)";

.media-generator(@sides...){
& when (length(@sides) = 0){
.loop-sizes(length(@media-sizes));
.loop-sizes(@screenIndex) when (@screenIndex > 0) {
& when (extract(@media-sizes, @screenIndex) = xs){
.padding-per-side(extract(@gutters, 1); left; right; bottom; top);
}
& when not (extract(@media-sizes, @screenIndex) = xs){
@condition: extract(@media-conditions, @screenIndex);
@media @condition{
.padding-per-side(extract(@gutters, @screenIndex); left; right; bottom; top);
}
}
.loop-sizes(@screenIndex - 1);
}
}
& when not (length(@sides) = 0){
.loop-sizes(length(@media-sizes));
.loop-sizes(@screenIndex) when (@screenIndex > 0) {
& when (extract(@media-sizes, @screenIndex) = xs){
.padding-per-side(extract(@gutters, 1); @sides);
}
& when not (extract(@media-sizes, @screenIndex) = xs){
@condition: extract(@media-conditions, @screenIndex);
@media @condition{
.padding-per-side(extract(@gutters, @screenIndex); @sides);
}
}
.loop-sizes(@screenIndex - 1);
}
}
}
.padding-per-side(@gutter; @sides...){
.loop-sides(length(@sides));
.loop-sides(@index) when (@index > 0){
@side: extract(@sides, @index);
&.@{side}{
padding-@{side}: @gutter;
}
.loop-sides(@index - 1);
}
}
.padding{
.media-generator(left; right); // specify sides if needed else leave blank
}


Related Topics



Leave a reply



Submit