0
votes

I am trying to make a variadic mixin in LESS. To this end I use the following syntax for my mixin :

.mixin (@a; @rest...) {
    // @rest is bound to arguments after @a   
    // @arguments is bound to all arguments 
}

But I don't know how to manipulate @rest and read to the last parameters of the mixin.

This is my code :

.color () { }

.color (@shade) {
    #id {
        background-color : rgb(@shade, @shade, @shade);
    }
}

.color (@shade, @rest...) {
    #id {
        background-color : rgb(@shade, @shade, @shade);
    }
    .color(@rest);
}

.color(200, 160);

As you guess, this mixin should examinate the entire list of parameters, and colour the background of my <div id="id"> with the shade of grey corresponding to the last parameter of the mixin (in this case, rgb(160, 160, 160)).

But when I compile this code with less-1.4.1.js, I get the following error :

SyntaxError: error evaluating function `rgb`: 
color functions take numbers as parameters

So how to access to the second, third, fourth... parameters of the mixin ?

Thanks a lot for your advices, and have a nice week-end !


EDIT

This works perfectly, thanks a lot !

However I would like to ask an other question. Let's say that my mixin is variadic to the extent that it requires at least one parameter which has nothing to do with the rest of the arguments (e.g. a string or an other number), but which has to be processed, so that possible calls to the previous mixin could be :

.color("first argument", 200, 160);
.color(-42, 200, 160);
.color(3, 200, 160); // 3 doesn't need to be processed in the loop

In other words, the .loop should examinate all the parameters of the mixin starting from the second and apply a different process to the first argument. So I need to change the skeleton of the mixin into something like this :

.color(...) {
   ...; // Handling the first parameter
   .loop (@arguments); // Handling the rest of the parameters
}

But in your solution, the variable @arguments contains the entire list of arguments, including the first. How to exclude it from the list, without playing on isnumber() ?

I precise that actually in my project, each of the parameters starting from the second are processed, so that :

.loop(@list, @index: 1, @shade: NULL) when not (isnumber(@list)) and (isnumber(extract(@list, @index))) {
   .loop(@list, (@index + 1), extract(@list, @index));
}

becomes

.loop(@list, @index: 1, @shade: NULL) when not (isnumber(@list)) and (isnumber(extract(@list, @index))) {
   .loop(@shade);
   .loop(@list, (@index + 1), extract(@list, @index));
}

and this process doesn't consist in simply changing the background color of a fixed <div> ;) But I wanted to simplify my question.

Thanks a lot for your answers and precious advices !


Edit, again : what you recommend to me works perfectly, Martin. Thanks again !

1

1 Answers

2
votes

Less gets confused with your second and third .color mixin, as they can both take just one argument, and if @rest is passed to the mixin as an argument and is not numeric (i.e. a list or empty) it causes more problems. Also @rest... and ... are tricky with multiple mixins with the same name - it is better to pass the arguments to another set of mixins (as a list to a single argument) and then switch between them using guards or the number of arguments they can take.

I would structure the mixins a bit differently (and add a helper mixin .loop that does the looping according to what is passed to .color).

The two mixins would then work like this:

  • .color: passes all arguments in @arguments to a single argument in mixin .loop
    • .loop: if the argument is neither a list nor a numeric value -> no output
    • .loop: if multiple arguments in a list -> loop through the list until you reach the last numeric value (or rather stops when it meets the first nonnumeric argument)
    • .loop: when reached the last parameter return output based on its value
    • .loop: if parameter is a single value -> return output based on the single parameter

in Less this could be done like this:

.loop(@list, @index: 1, @shade: NULL) when not (isnumber(@list)) and not (isnumber(extract(@list, @index))) and not (isnumber(@shade)) {}

.loop(@list, @index: 1, @shade: NULL) when not (isnumber(@list)) and (isnumber(extract(@list, @index))) {
   .loop(@list, (@index + 1), extract(@list, @index));
}

.loop(@list, @index: 1, @shade) when not (isnumber(@list)) and not (isnumber(extract(@list, @index))) {
    .loop(@shade);
}

.loop(@shade) when (isnumber(@shade)){
    #id {
        background-color: rgb(@shade, @shade, @shade);
    }
}

.color (...) {
    .loop(@arguments);
}

and if you call now something like this:

.color(120,160);

the output CSS will look like this:

#id {
  background-color: #a0a0a0;
}

which corresponds to the value of the last argument -> rgb(160,160,160)

This now has an output only for the last argument in the list. If you want to do something for each argument, you do so in the second .loop mixin (the actual loop) and can get rid of the third one that is used only to separate the last iteration step from the rest of the loop.


Edit:

To your additional question "How to deal differently with certain arguments and what to do when they are not numeric?" - > General answer: you can always adjust the guards, and add additional mixins for the specific cases.

For example, if you want to treat the first argument (or any argument) differently using the .loop, you can add an additional .loop mixin - like this:

.loop(@list) when not (isnumber(@list)) {
   /* do something with the first argument */
   @first_argument: extract(@list, 1);
   /* start the loop at the second argument:*/
   .loop(@list, 2);
}

with the rest left as is, this should do what you want:

  • save your "first argument" from the @arguments into a variable (or you can do with it whatever you want)
  • start the loop at the second argument and continue until it reaches the last numeric argument (as shown above)

As said before, this is just an example illustrating how to do what you asked ... but you will have to play with it and design the guards and mixins accordingly to your desired outcome and your specific problems.

In the loop you can do something to each argument ... and you can do different things to numeric and nonnumeric arguments (you just need to adjust the conditions in the guards), you can check also if the arguments have specific units and so on. All this is simple as you are merely iterating through a list using the extract() function and incrising the @index variable.

In the case the first argument / first couple arguments have a specific assignment you can use @rest in the same way as I show with @arguments and do something with the first argument already in the .color mixin before you send the @rest to the .loop. You would do something like this:

.color (@first, @rest...) {
   /* do something with the first argument */
   @first_argument: @first;
   /* send the rest to the loop */
   .loop(@rest);
}