LESS Mixin for Multiple Transition Properties under Same duration, timing-function and delay

Preface

Sometimes in CSS we’d like to transition on multiple properties under the same transition duration, timing function and delay, and we’d like to transition only those specific properties so we can’t use all as transition-property. Like this, for something that we probably want transitioning effect for properties pertaining to the y axis but not for those pertaining to the x axis:

.box {
  transition: height 0.4s linear,
              background-position-y 0.4s linear,
              padding-top 0.4s linear;
}

Of course, that’s some duplicated code. One way to reduce this duplicated code is to not use the shorthand rule:

.box {
  transition-property: height, background-position-y, padding-top;
  transition-duration: 0.4s;
  transition-timing-function: linear;
}

But in this post we’ll learn how to use the shorthand rule while reducing code duplication through CSS preprocessors: The exercise today is to write a LESS mixin that takes an arbitrary number of property names and produces the shorthand rule, e.g. something like .uniform-transition(0.4s linear, height, background-position-y, padding-top);

First Attempt (and Failing)

To achieve this, LESS’s @rest variable can be used, like in the following skeleton code:

.uniform-transition(@params, @rest...) { }

.first-box {
  .uniform-transition(0.4s linear 0.1s, height, width);
}

.second-box {
  .uniform-transition(0.4s linear 0.1s, height);
}

When the .uniform-transition() mixin is called by .first-box, @rest will be height and width, and when called by .second-box, it will be height.

To glue the transition properties inside @rest with the @params, we’ll need some JavaScript evaluation inside LESS, which is a less documented feature (well, I should say “not a LESS documented feature”, right?). Speaking at a higher level, the JavaScript function we want transforms ['height', 'width'] into ['height 0.4s linear 0.1s', 'width 0.4s linear 0.1s'] and then join the array elements with a comma. Sounds simple…

Except that @rest in the mixin does not have the same syntax when there is one rest argument versus when there are multiple rest arguments. When there is one rest argument, when @rest is serialized for JavaScript evaluation, it’s just the argument value (like height). When there are multiple rest arguments, when @rest is serialized, the arguments will be comma-space-separated and surrounded by brackets (like [height, width]) (Note we’re talking about how LESS serializes variables, where there are no quotes around the serialized arguments.)

With that discrepancy in mind, the JavaScript function want would be like the following:

// I'm using ES5 in case people haven't enabled harmony on their nodejs ;)
function(restArgsStr) {
  // We can also use restArgs = restArgsStr.replace(/\[([^\]]+)\]/, '$1').split(',');
  var restArgs = restArgsStr.substr(0, 1) === '[' ?
    restArgsStr.substr(1, restArgsStr.length - 2).split(',') :
    [restArgsStr];

 // of course, the "0.4s linear 0.1s needs" will be interpolated/serialized from @params
 return restArgs
          .map(function(arg){return arg + ' 0.4s linear 0.1s';})
          .join(',');
}

Now we just need to use LESS’s string interpolation + JavaScript evaluation mechanism to get the result.

Note, if you’re using LESS version > 3.0.0, you’ll need to turn on a flag to enable inline JavaScript.

// LESS string interpolation can be multi-line, so we don't have to tuck everything into one unreadable chunk of line.
.uniform-transition(@params, @rest...) {
  // Note that LESS's JavaScript evaluation requires an expression, so we have an IIFE there.
  transition: ~`(function(propsStr){
                   var props = propsStr.substr(0, 1) === '[' ?
                     propsStr.substr(1, propsStr.length - 2).split(',') :
                     [propsStr];
                   return props
                     .map(function(prop){ return prop + ' @{params}'; })
                     .join(',');
                 })('@{rest}')`; // don't forget the quotes around @{rest}.

  // Or if using the regular-expression approach, we also can forego the IIFE:
  transition: ~`'@{rest}'
                  .replace(/\[([^\]]+)\]/, '$1')
                  .split(',')
                  .map(function(prop){ return prop + ' @{params}'; })
                  .join(',')`;
}

.first-box {
  .uniform-transition(0.4s linear 0.1s, height, width);
}

.second-box {
  .uniform-transition(0.4s linear 0.1s, height);
}

It turns out that the code snippet above does not work, and upon investigation we’ll see that, when interpolated, @{params} gets serialized as [0.4s, linear, 0.1s] (and if we have only one component there, it would be something like 0.4s, without the brackets, just like @rest we talked about above).

No good, right?

Second, Correct Attempt

We can solve this problem through two approaches: First one is to reuse the transform-to-array technique we applied for propsStr, and the other one being we actually use another LESS specialized mixin variable called @arguments. This variable behaves much like JavaScript’s arguments variable, and for the .first-box usage there, it will be serialized as [0.4s linear 0.1s, height, width]; if we write .uniform-transition(0.4s linear 0.1s, height); then the serialization would be [0.4s, height]. With this approach, we actually have one bonus: Since the .uniform-transition() mixin always accepts more than one arguments, we don’t need the transform-to-array technique anymore, except that as @arguments is now heterogeneous (the first component has different meaning than the rest), we lose some clarity there.

The final code is:

.uniform-transition(...) {
  transition: ~`(function(lessArgsStr){
                   var lessArgs = lessArgsStr.substr(1, lessArgsStr.length - 2).split(','); // Or use regexp
                   return lessArgs
                     .splice(1)
                     .map(function(prop){ return prop + ' ' + lessArgs[0]; })
                     .join(',');
                 })('@{arguments}')`;
}

.first-box {
  .uniform-transition(0.4s linear 0.1s, height, width);
}

.second-box {
  .uniform-transition(0.4s linear 0.1s, height);
}

.third-box {
  .uniform-transition(0.4s, height);
}

Check the result at the JSFiddle (inspect the result’s DOM to see the compiled styles).

Appendix and Tips

If we want to see how a variable is serialized by LESS when interpolated into a JavaScript evaluation string, we can write something like ~`console.log('${variable}'), 1`. We’re using the lesser-known characteristic of the comma operator in C-like languages, where an expression of a, b, c evaluates to c. And thus, that “1” can be any arbitrary CSS value. If we write ~`console.log('${variable}')` without the , 1 we’ll get JavaScript error since console.log doesn’t return anything and would result in LESS trying to operate on undefined.

Finally, it’s worth noting while answers to a StackOverflow question gives what the OP wants, the OP is using a style that probably won’t give desired effect: transition: color, opacity .5s ease-in-out; will not have have transition-duration or transition-timing-function applied to color. Essentially, color will have these two transition properties set to initial, and with no other cascaded styles, they’ll be 0s and ease by default. See example at JSFiddle.

發表迴響

你的電子郵件位址並不會被公開。 必要欄位標記為 *