Can I define a LESS mixin to generate a transition-property with a variable number of parameters?

StackOverflow https://stackoverflow.com/questions/10412643

  •  05-06-2021
  •  | 
  •  

Question

I'm introducing LESS to a large web app project to simplify my CSS. I've got a few CSS rules which apply transitions to a varying number of properties, for example:

.movable {
    transition-property: top, left;
    transition-duration: 0.2s;
    transition-timing-function: ease;
}

.fadeAndStretchable {
    transition-property: opacity, width, height, margin;
    transition-duration: 1.5s;
    transition-timing-function: ease-out;
}

(Note: I've omitted -webkit, -moz and -o properties here for brevity: in reality each of these rules is 12 lines long rather than 3.)

Note that the values for transition-property are comma-separated. This is unusual in CSS: multiple values are usually space-separated (as in border: 1px solid #f00). LESS mixins can use the special @arguments value to produce a space-separated list of all the mixin arguments - but is it possible to define a LESS mixin that takes a variable number of parameters and turns them into a comma-separated value list, suitable for transition-property?

If necessary, I'm happy with a solution that requires two mixins: one for transition-property and another for transition-duration and transition-timing-function. Here's what I've tried so far:

Attempt 1: using @arguments with unnamed parameters

.transition-property() {
    -webkit-transition-property: @arguments;
    -moz-transition-property: @arguments;
    -o-transition-property: @arguments;
    transition-property: @arguments;
}

.movable {
    .transition-property(top, left);
}

Result: LESS error ("No matching definition was found for '.transition-property(top, left)'")

Attempt 2: using @arguments with named parameters

.transition-property(@p1, @p2, @p3, @p4, @p5) {
    -webkit-transition-property: @arguments;
    -moz-transition-property: @arguments;
    -o-transition-property: @arguments;
    transition-property: @arguments;
}

.movable {
    .transition-property(top, left);
}

Result: LESS error ("No matching definition was found for '.transition-property(top, left)'")

Attempt 3: using named parameters with dummy default values

.transition-property(@p1:p1, @p2:p2, @p3:p3, @p4:p4, @p5:p5) {
    -webkit-transition-property: @p1, @p2, @p3, @p4, @p5;
    -moz-transition-property:  @p1, @p2, @p3, @p4, @p5;
    -o-transition-property:  @p1, @p2, @p3, @p4, @p5;
    transition-property:  @p1, @p2, @p3, @p4, @p5;
}

.movable {
    .transition-property(top, left);
}

Result: No LESS error but it generates a CSS rule -webkit-transition-property: top, left, p3, p4, p5 that the browser ignores because of the unrecognised properties.

I've tried various other approaches (e.g. passing the property as a string 'top,left') but all result in the same thing: either a LESS error or invalid CSS.

Is there any way round this? Or do I have to bite the bullet and define a set of mixins overloaded on arity, e.g.

.transition-property(@p1) {...}
.transition-property(@p1, @p2) {...}
.transition-property(@p1, @p2, @p3) {...}
.transition-property(@p1, @p2, @p3, @p4) {...}
etc.
Was it helpful?

Solution

I've managed to figure it out thanks to Luke Page pointing me towards the ... syntax.

The solution was to use the following:

Phew. Here's the resulting mixin:

.transition-properties(...) {
    -webkit-transition-property: ~`"@{arguments}".replace(/[\[\]]/g, '')`;
}

And here's the full version with a complete set of browser extensions:

.transition-properties(...) {
    @props: ~`"@{arguments}".replace(/[\[\]]/g, '')`;
    -webkit-transition-property: @props;
    -moz-transition-property: @props;
    -o-transition-property: @props;
    transition-property: @props;
}

OTHER TIPS

Perhaps I am misunderstanding your needs. Why can you not use an escaped string?

Like so:

.transition ( @property, @duration, @style: ease-in-out ) {
  -webkit-transition-property: @property;  
  -webkit-transition-duration: @duration;
  -webkit-transition-timing-function: @style;

  -moz-transition-property: @property;  
  -moz-transition-duration: @duration;
  -moz-transition-timing-function: @style;

  -ms-transition-property: @property;  
  -ms-transition-duration: @duration;
  -ms-transition-timing-function: @style;

  -o-transition-property: @property;  
  -o-transition-duration: @duration;
  -o-transition-timing-function: @style;

  transition-property: @property;  
  transition-duration: @duration;
  transition-timing-function: @style;
}

#my-id {
  .transition( ~"background, border-color, color", 2s );
}

This is exactly what we use for multi-property transitions. Never had a problem with it.

Flexible (LESS 1.5.1+)

This solution does not use any inline javascript and it allows for:

  1. Defaults to be set
  2. Any number of properties, durations, delays, etc., to be passed
  3. Output either in long form or compact form
  4. A raw list input instead of groups of parameters being input if desired

If the number of properties are greater than the number of durations, delays, or timings, then if the compact output is set, the final value for duration/delay/timing becomes the value for that parameter for all additional properties beyond the number passed, but if compact is not set, then the long form is output and values are duplicated per browsers interpret ion of the css standards.

LESS Mixin

.transition (@props: all; 
             @duration:1s; 
             @delay: 0s; 
             @timing: ease; 
             @compact: true;
             @raw-input: false) {
  .output() when (@raw-input = false) and not (@compact = true) {
  -webkit-transition-property:@props; 
     -moz-transition-property:@props;
      -ms-transition-property:@props;
       -o-transition-property:@props; 
          transition-property:@props;
  -webkit-transition-duration:@duration; 
     -moz-transition-duration:@duration;
      -ms-transition-duration:@duration;
       -o-transition-duration:@duration; 
          transition-duration:@duration;
  -webkit-transition-delay:   @delay; 
     -moz-transition-delay:   @delay;
      -ms-transition-delay:   @delay;
       -o-transition-delay:   @delay; 
          transition-delay:   @delay;
  -webkit-transition-timing-function:@timing; 
     -moz-transition-timing-function:@timing;
      -ms-transition-timing-function:@timing;
       -o-transition-timing-function:@timing; 
          transition-timing-function:@timing;
  }
  .output() when (@raw-input = false) and (@compact = true) {
    @propsLength: length(@props);
    @durationLength: length(@duration);
    @delayLength: length(@delay);
    @timingLength: length(@timing);
    .buildString(@i, @s: ~'') when (@i <= @propsLength) {
      @prop: extract(@props, @i);
      .setDuration() when (@i <= @durationLength) {
        @dur: extract(@duration, @i);
      }
      .setDuration() when (@i > @durationLength) {
        @dur: extract(@duration, @durationLength);
      }
      .setDuration();
      .setDelay() when (@i <= @delayLength) {
        @del: extract(@delay, @i);
      }
      .setDelay() when (@i > @delayLength) {
        @del: extract(@delay, @delayLength);
      }
      .setDelay();
      .setTiming() when (@i <= @timingLength) {
        @time: extract(@timing, @i);
      }
      .setTiming() when (@i > @timingLength) {
        @time: extract(@timing, @timingLength);
      }
      .setTiming();
      .setDivider() when (@i > 1) {
        @divider: ~'@{s},';
      }
      .setDivider() when (@i = 1) {
        @divider: ~'';
      }
      .setDivider();
      @string: @divider @prop @dur @del @time;
      .buildString((@i + 1), @string);  
    }
    .buildString(1);
    .buildString(@i, @s: ~'') when (@i > @propsLength) {
      .compact(@s);
    }
  }
  .output() when not (@raw-input = false) {
    .compact(@raw-input);
  }
  .compact(@string) {
    -webkit-transition:@string; 
       -moz-transition:@string;
        -ms-transition:@string;
         -o-transition:@string; 
            transition:@string;    
  }
  .output();
} 

LESS Use Examples

.test {
  .transition();
}
.test-props {
  .transition(width);
}
.test-duration {
  .transition(@duration: 3s);
}
.test-delay {
  .transition(@delay: 10s);
}
.test-timing {
  .transition(@timing: linear);
}
.test-all {
  .transition(height, 4s, 12s, ease-out);
}
.test-multitransitions {
  .transition(width, height, top; 1s, 2s; 0s, 1s, 3s; ease-in, ease-out, ease);
}
.test-not-compact {
  .transition(width, height, top; 1s, 2s; 0s, 1s, 3s; ease-in, ease-out, ease; false);
}
.test-raw-input {
  .transition(@raw-input: top 1s, bottom 1s, color 3s 1s linear;);
}

In the above examples, note two things in particular: (1) how the multiple values need to be passed using commas to separate the lists, but semicolons to separate the parameter groups. So to visualize, it is this:

  .transition(width, height, top; 1s, 2s; 0s, 1s, 3s; ease-in, ease-out, ease);
              |---Properties----|-Dur.--|---Delay---|---------Timing--------|
                                |       |           |
                          semicolons divide groups of parameters

(2) how the raw-input example needs an ending semicolon to have it consider the commas as list items:

  .transition(@raw-input: top 1s, bottom 1s, color 3s 1s linear;);
                                                               |
                                                    semicolon here needed

CSS Output of Examples

.test {
  -webkit-transition:  all 1s 0s ease;
  -moz-transition:  all 1s 0s ease;
  -ms-transition:  all 1s 0s ease;
  -o-transition:  all 1s 0s ease;
  transition:  all 1s 0s ease;
}
.test-props {
  -webkit-transition:  width 1s 0s ease;
  -moz-transition:  width 1s 0s ease;
  -ms-transition:  width 1s 0s ease;
  -o-transition:  width 1s 0s ease;
  transition:  width 1s 0s ease;
}
.test-duration {
  -webkit-transition:  all 3s 0s ease;
  -moz-transition:  all 3s 0s ease;
  -ms-transition:  all 3s 0s ease;
  -o-transition:  all 3s 0s ease;
  transition:  all 3s 0s ease;
}
.test-delay {
  -webkit-transition:  all 1s 10s ease;
  -moz-transition:  all 1s 10s ease;
  -ms-transition:  all 1s 10s ease;
  -o-transition:  all 1s 10s ease;
  transition:  all 1s 10s ease;
}
.test-timing {
  -webkit-transition:  all 1s 0s linear;
  -moz-transition:  all 1s 0s linear;
  -ms-transition:  all 1s 0s linear;
  -o-transition:  all 1s 0s linear;
  transition:  all 1s 0s linear;
}
.test-all {
  -webkit-transition:  height 4s 12s ease-out;
  -moz-transition:  height 4s 12s ease-out;
  -ms-transition:  height 4s 12s ease-out;
  -o-transition:  height 4s 12s ease-out;
  transition:  height 4s 12s ease-out;
}
.test-multitransitions {
  -webkit-transition:  width 1s 0s ease-in, height 2s 1s ease-out, top 2s 3s ease;
  -moz-transition:  width 1s 0s ease-in, height 2s 1s ease-out, top 2s 3s ease;
  -ms-transition:  width 1s 0s ease-in, height 2s 1s ease-out, top 2s 3s ease;
  -o-transition:  width 1s 0s ease-in, height 2s 1s ease-out, top 2s 3s ease;
  transition:  width 1s 0s ease-in, height 2s 1s ease-out, top 2s 3s ease;
}
.test-not-compact {
  -webkit-transition-property: width, height, top;
  -moz-transition-property: width, height, top;
  -ms-transition-property: width, height, top;
  -o-transition-property: width, height, top;
  transition-property: width, height, top;
  -webkit-transition-duration: 1s, 2s;
  -moz-transition-duration: 1s, 2s;
  -ms-transition-duration: 1s, 2s;
  -o-transition-duration: 1s, 2s;
  transition-duration: 1s, 2s;
  -webkit-transition-delay: 0s, 1s, 3s;
  -moz-transition-delay: 0s, 1s, 3s;
  -ms-transition-delay: 0s, 1s, 3s;
  -o-transition-delay: 0s, 1s, 3s;
  transition-delay: 0s, 1s, 3s;
  -webkit-transition-timing-function: ease-in, ease-out, ease;
  -moz-transition-timing-function: ease-in, ease-out, ease;
  -ms-transition-timing-function: ease-in, ease-out, ease;
  -o-transition-timing-function: ease-in, ease-out, ease;
  transition-timing-function: ease-in, ease-out, ease;
}    
.test-raw-input {
  -webkit-transition: top 1s, bottom 1s, color 3s 1s linear;
  -moz-transition: top 1s, bottom 1s, color 3s 1s linear;
  -ms-transition: top 1s, bottom 1s, color 3s 1s linear;
  -o-transition: top 1s, bottom 1s, color 3s 1s linear;
  transition: top 1s, bottom 1s, color 3s 1s linear;
}

If long form is never desired then the mixin code can reduce to this:

.transition (@props: all; 
             @duration:1s; 
             @delay: 0s; 
             @timing: ease; 
             @raw-input: false) {
  .output() when (@raw-input = false) {
    @propsLength: length(@props);
    @durationLength: length(@duration);
    @delayLength: length(@delay);
    @timingLength: length(@timing);
    .buildString(@i, @s: ~'') when (@i <= @propsLength) {
      @prop: extract(@props, @i);
      .setDuration() when (@i <= @durationLength) {
        @dur: extract(@duration, @i);
      }
      .setDuration() when (@i > @durationLength) {
        @dur: extract(@duration, @durationLength);
      }
      .setDuration();
      .setDelay() when (@i <= @delayLength) {
        @del: extract(@delay, @i);
      }
      .setDelay() when (@i > @delayLength) {
        @del: extract(@delay, @delayLength);
      }
      .setDelay();
      .setTiming() when (@i <= @timingLength) {
        @time: extract(@timing, @i);
      }
      .setTiming() when (@i > @timingLength) {
        @time: extract(@timing, @timingLength);
      }
      .setTiming();
      .setDivider() when (@i > 1) {
        @divider: ~'@{s},';
      }
      .setDivider() when (@i = 1) {
        @divider: ~'';
      }
      .setDivider();
      @string: @divider @prop @dur @del @time;
      .buildString((@i + 1), @string);  
    }
    .buildString(1);
    .buildString(@i, @s: ~'') when (@i > @propsLength) {
      .compact(@s);
    }
  }
  .output() when not (@raw-input = false) {
    .compact(@raw-input);
  }
  .compact(@string) {
    -webkit-transition:@string; 
       -moz-transition:@string;
        -ms-transition:@string;
         -o-transition:@string; 
            transition:@string;    
  }
  .output();
}

From less.js 1.3 onwards you have to specify ... in the argument list to signify that more arguments can be added. e.g.

.transition-property(...) {
 foo: @arguments;
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top