Passing Rulesets To Mixins In Less CSS
Recently, I've been making a concerted effort to actually learn more about Less CSS - the CSS preprocessor. As I was reading through the documentation, I came across the ability to pass rulesets to mixins where they could be evaluated. I was having trouble wrapping my head around the concept, so I thought I would play around. My first thought was to try and treat the ruleset like a block in Ruby that could be used to iterate over a list of items.
NOTE: I am not sure if I am using the term "block" correctly; I am not a Ruby developer - don't tase me, bro.
Given a list of elements, I wanted to see if I could generate a CSS class for each item in the list. Furthermore, I wanted to see if I could generate AngularJS-inspired iteration variables like @first, @last, and @mid. In the end, this actually ended up being a really cool challenge because it touched on a number of Less CSS features:
- JavaScript execution.
- List data types.
- List iteration.
- List manipulation.
- Recursive looping.
- Embedded list arguments.
- Rulesets.
- Dynamic, block-based scoping.
- Variable interpolation.
- Unit math.
To do this, I created a mixin, .list-loop, which takes a list and a ruleset. It then iterates over the loop [recursively] and "invokes" the ruleset for each item in the list. During the iteration, I'm making the @{it} variable point to the current item in the iteration. This value is then being used, inside the ruleset (thanks to scoping), to dynamically generate class names and properties in the resultant CSS.
// I loop over the given list and apply the given ruleset on each iteration. During
// the iteration, the loop context provides several variables:
// --
// @it - the current item of iteration.
// @index - the current iteration index.
// @length - the length of the list being iterated.
// @first - is true on the first iteration of the loop.
// @last - is true on the last iteration of the loop.
// @mid - is true when the iteration is neither first nor last.
// --
.list-loop( @list ; @ruleset ) {
@length: length( @list ) ;
// Initiate the loop.
.looper( 1 ) ;
// I am the recursive portion of the loop.
.looper( @index ) when ( @index <= @length ) {
// Set the current value of the iteration.
@it: extract( @list, @index ) ;
// Set values that the rules can used for context.
// --
// NOTE: This is using JavaScript since LESS doesn't seem to allow me to set
// the result of an equality check.
@first: `( @{index} == 1 )` ;
@last: `( @{index} == @{length} )` ;
@mid: `! ( @{first} || @{last} )` ;
// Invoke the given ruleset.
// --
// CAUTION: All variables defined in this set of mixins will be made
// available to the scope of the ruleset when it is evaluated.
@ruleset();
// Call recursively for next iteration.
.looper( @index + 1 ) ;
}
}
// ---------------------------------------------------------- //
// ---------------------------------------------------------- //
// When looping over the list, @{it} contains the current item in the iteration.
// The ruleset will then be "invoked" for each value of @{it}.
.list-loop(
alpha, beta, theta, omega ;
{
span.@{it} {
background-position: ( ( @index - 1 ) * 50px ) 0px ;
content: "@{it} is the awesome" ;
& when ( @first ) {
border-top: 0px ;
}
& when ( @last ) {
border-bottom: 0px ;
}
}
}
);
As you can see, I'm using variable interpolation to generate the class names and the "content" property. When we compile the above Less CSS, we get the following CSS output:
span.alpha {
background-position: 0px 0px;
content: "alpha is the awesome";
border-top: 0px ;
}
span.beta {
background-position: 50px 0px;
content: "beta is the awesome";
}
span.theta {
background-position: 100px 0px;
content: "theta is the awesome";
}
span.omega {
background-position: 150px 0px;
content: "omega is the awesome";
border-bottom: 0px ;
}
While I was able to the @first and @last variables to dynamically add CSS properties, I'm not sure it really makes any sense in this context; but, like I said, I'm just trying to learn more about the Less CSS preprocessor. Since Less CSS tries really hard to look like normal CSS, the syntax is a bit ... odd; but, it seems to be pretty powerful.
Want to use code from this post? Check out the license.
Reader Comments
Note: In the video, I stated that I thought the mixin variables were available in the ruleset since the ruleset was running in the context of the mixin. I think that was incorrect. According to the docs, any variables defined in the mixin are simply available in the calling context:
> All variables defined in a mixin are visible and can be used in caller's scope (unless the caller defines its own variable with the same name).
I believe *this* is why the @first, @last, and so-on, are available in the ruleset. But, I am not 100% sure.
Ben,
Thanks for doing this series on Less.
Every single day I still find out many people choose to not use a pre-compiler for one reason or another.
I think there needs to be more advocates of your caliber pushing the use of a pre-processor, especially LESS.
Less is beautiful because it's easiest to jump into and not feel guilty due to it following the CSS syntax to a T.
I love all the work you do, and occasionally want to remind you of that.
-Josh
@Joshua,
I really appreciate that! I've been using LESS fairly "lightly" for the last 2 years. By lightly, I mean that I've basically been using the nesting capabilities and some Bootstrap mixins. That said, even that usage has been a **game changer**. CSS preprocessing is just awesome! And you can tell that it's so awesome because it's taken me 2 years of light use before I even felt the real desire to dig deeper. Even the simple stuff makes such a huge difference.
You're very welcome.
And it's getting a lot easier for people to use it.
Less easier for people to make excuses for not using it.
Simply dropping Grunt into an existing workflow can change a project's maintainability drastically.
Keep it up, thanks for digging deeper!
@Joshua,
Grunt is one of the things I gotta check out as well; and Gulp. I hear great stuff about that. The whole "build" ecosystem is relatively new. I only really got into Less because there are apps that do it for you (ie, LiveReload, Less.app, CodeKit, etc).
Just in case, you don't need any inline javascript to achieve that, see: http://www.pnml.kz/2014/06/ for example. In fact all this is/was possible w/o the "detached rulesets" feature (see https://github.com/seven-phases-max/less.curious/blob/master/articles/for-each.md).
I found out that there is one problem with your mixin.
If you fire the mixin twice the variables are fixed on the first iteration.
Solution for this is to actually reassign the index variable instead of just counting it up.
.looper( @index: @index + 1 ) ;
Thank you so much for that, work's perfectly! Cheers! :D