
Surprisingly quietly and without much fanfare a groundbreaking change in the LESS world recently happened: so-called detached rulesets had arrived in version 1.7. In essence it means an ability to store a block of LESS code into variable or pass it as an argument to mixin.
According to documentation, a primary motivation for such a feature is @media
queries with overlapping rules, but certainly there are many other useful applications possible.
For instance it alleviates a LESS’ inability to call a mixin indirectly by name – now we can supply a whole mixin invocation in curly braces instead of just a name.
The most interesting feature of the detached rulesets is their scope: the code inside them has access to both contexts – where ruleset was defined and where it’s used.
In combination with LESS’ distinctive lazy evaluation it allows us to use mixin ruleset arguments as some kind of “lambda-expressions” or “anonymous mixins” after similar concept of anonymous classes in java.
Let’s take iteration of lists or numeric ranges as an example. Perhaps that’s a little uncommon task in a day to day stylesheet authoring but it should be quite demonstrative.
Basic looping is trivially achieved with a recursive mixin. It’s really simple but only if fully implemented in each place. In pre-1.7 time any attempt to generalize the recursive part and move it to mixin library had to take the same design decisions: a loop body must be a mixin, no simple way to call mixin by name implies that name should be known beforehand (hardcoded in library mixin), that leads to clash of the same-named mixins when several loops are defined nearby, so wrapping each loop in additional block to limit its scope is suggested.
By the way, there exists a LESS-idiom for such scoping purposes – a nested rule with single parent selector: & { … }
Probably the most advanced of pre-1.7 iteration mixin libraries is less.curious from one of core LESS developers @seven-phases-max. That’s a really clever abuse of LESS’ syntax, source formatting and laziness. Here is an example from its documentation:
#icon {
.for(home ok cancel error book); .-each(@name) {
&-@{name} {
background-image: url("../images/@{name}.png");
}
}
}
Now that we’ve got detached rulesets, we can easily write the code that produces exactly same results like this:
.foreach(home ok cancel error book, {
#icon-@{item} {
background-image: url("../images/@{item}.png");
}
});
Not having to name the loop body mixin, we can get rid of the wrapping block while retaining the ability to place several loops in the same context, as we can limit the scope of recursive mixins inside the library mixin.
That’s how such mixin might look:
.foreach(@array, @lambda)
{
&
{
.iterator(@index) when (@index > 1)
{
.iterator((@index - 1));
}
.iterator(@index)
{
@item: extract(@array, @index);
@lambda();
}
@length: length(@array);
.iterator(@length);
};
}
It takes two arguments, first one is a list to iterate and second one is a ruleset. Inside that ruleset following variables are available: @item
– current list item, @index
– its position in the list, @array
– the list itself and @length
– total amount of list items. Indices are starting from one, matching behavior of built-in “extract
” function.
Caution! Names collision possible – it’s better to avoid the use of such variable names in other places.
When reffering to mixin or variable LESS search it in defining context before the usage context (quite illogical, in my opinion, but unlikely to change as it can broke many LESS users code in subtle ways and cause the debugging nightmare). So one can break all the loops in project simply by having a variable named “@item
” defined in top context. Those collisions are probably the most significant drawback of this approach. It can be somewhat obviated by prefixing variable names though.
Here are some more usage examples in codepen:
// generic list iteration mixin for LESS 1.7
// arguments:
// @array - list of items to iterate
// @lambda - ruleset with loop body
// inside @lambda block folowwing variables are available:
// @item – current list item
// @index – its position in the list
// @array – the list itself and
// @length – total amount of list items
// more info: http://www.pnml.kz/2014/06/detached-rulesets-in-less-as-a-lambda-expressions/
.foreach(@array, @lambda)
{
&
{
.iterator(@index) when (@index > 1)
{
.iterator((@index - 1));
}
.iterator(@index)
{
@item: extract(@array, @index);
@lambda();
}
@length: length(@array);
.iterator(@length);
};
}
// usage examples
.foreach(#a6c8e0 #fecc94 #c8e793 #b2e1d9 #f9f9ad,
{
li:nth-child(@{length}n+@{index}) { background: @item; }
});
.foreach(home ok cancel error book, {
#icon-@{item} {
background-image: url("../images/@{item}.png");
}
});
See the Pen generic list iteration mixin for LESS 1.7 by bsl-zcs (@bsl-zcs) on CodePen.
Besides iteration, detached rulesets potentially can alter the ways people use LESS, exactly like lambdas in Java 8. Some places where mixins are usually used can be switched to rulesets and comparative simplicity of their use could lead to modularity increasing.
As LESS’ scope resolution logic prevents making mixins overridable in nested contexts, a ruleset variable with a default value can eventually overtake the role of the main instrument for optional style tuning.