This document outlines how Sass plans to handle passing a variable number of arguments to mixins and functions. These features will be rolled out over two releases. In Sass 3.2, we will release a minimal implementation that handles positional arguments very nicely and keyword style arguments partially. This is enough to handle the most common use cases.
In Sass 3.3, we will add a new data structure representing a map/hash/associative array, this will enable a complete solution to receive and handle unbound named arguments passed to a function or array. The syntax and API for maps is still in flux at this time, so consider any syntax used in this example directional.
This document explains the complete syntax and usage of this feature. Aspects that will not be implemented in 3.2 are annotated.
There are two parts to variable arguments. The first is the ability to recieve some or all of the arguments passed to a mixin or function and to be able to work with those values as a collection of data bound to a single local variable instead of as individual local variables.
The second part is the ability to pass a collection of data to a mixin and to have that data be interpreted as arguments to the mixin or function.
In both cases, ellpisis (3 dots: ...
) is used to indicate that you are receiving or sending
variable arguments.
On the declaration side, the ellipsis will precede the variable that is recieving the variable
arguments: ...$args
. On the calling side, the ellipsis trail the list or map variable
that contains the arguments: $args...
.
Open Question: Alternate forms of the ellipsis syntax might have the caller have preceding ellipsis and the receiver have the trailing ellipsis. Or, both sides could use a trailing ellipsis.
Note: All of these examples will use mixins, but the same syntax and semantics will apply
to @function
arguments.
When the last argument to a mixin is a variable followed by an ellipsis, that variable will receive all of the arguments passed which were not bound to the other explicitly declared arguments. This special variable is called the "Argument List Variable" or arglist for short. The arglist variable must be the last variable in the declaration.
The most simple case is where all arguments are received as a single variable
@mixin box-shadow(...$shadows) {
-moz-box-shadow: $shadows;
-webkit-box-shadow: $shadows;
box-shadow: $shadows;
}
.shadowed { @include box-shadow(0px 4px 5px #666, 2px 6px 10px #999) }
becomes the following output:
.shadowed {
-moz-box-shadow: 0px 4px 5px #666, 2px 6px 10px #999;
-webkit-box-shadow: 0px 4px 5px #666, 2px 6px 10px #999;
box-shadow: 0px 4px 5px #666, 2px 6px 10px #999;
}
The argument list variable primarily acts exactly like a comma delimited list
and can be used as list in all contexts. However the list is a special list
which is transparently holding the keywords inside it. To access the keyword
arguments as a map, you must use the keywords()
function.
@mixin width-selectors(...$args) {
// Note: type-of($args) == list
$keywords: keywords($args);
@each $k in keys($keywords) {
// Note: type-of($k) == string
.column-#{$k}{ width: fetch($keywords, $k);}
}
}
@include width-selectors($small: 40px, $medium: 100px, $large: 350px);
Generates:
.column-small { width: 40px }
.column-medium { width: 100px }
.column-large { width: 350px }
Note: The keywords function returns a map, so it will not be implemented until Sass 3.3.
It's possible to combine variable arguments syntax with named arguments.
@mixin grid-system($column-width, $gutter-width, ...$breakpoints) {
// Generate your grids
}
@include grid-system(50px, 15px, 320px, 480px, 800px);
Mixins that accept variable arguments will always accept any number positional and keyword arguments.
In order to disallow one type or the other, Sass will add a new @error
directive which is
exactly like @warn
, except it will cause the compilation to fail.
@mixin box-shadow(...$shadows) {
@if has-keywords($shadows) {
@error "The box-shadow mixin does not accept keyword arguments."
}
-moz-box-shadow: $shadows;
-webkit-box-shadow: $shadows;
box-shadow: $shadows;
}
@include box-shadow($unexpected: true); // Raises an error.
or to disallow positional arguments:
@mixin only-keywords(...$args) {
@if length($args) > 0 {
@error "The only-keywords mixin does not accept positional arguments."
}
// ...
}
When you have lists or maps of data that you need to pass as arguments, the ellipsis can be used to expand it into arguments. Ellipses can be used any number of times by the caller and at any place in the argument list.
You can bind a list of values to positional arguments by preceding the list with ellipsis. Here are some examples of how you can
$some-value: 100px;
$list-1: 1px, 2px, 3px;
$list-2: 10px 20px 30px; // space separated lists can also be used as an argument list.
@include some-mixin($list-1...) // some-mixin(1px, 2px, 3px)
@include some-mixin($list-1..., $list-2...) // some-mixin(1px, 2px, 3px, 10px, 20px, 30px)
@include some-mixin($some-value...) // some-mixin(100px)
@include some-mixin($list-1..., -5px) // some-mixin(1px, 2px, 3px, -5px)
@include some-mixin(-5px, $list-1...) // some-mixin(-5px, 1px, 2px, 3px)
@include some-mixin(-5px, $list-1..., -10px, $list-2...) // some-mixin(-5px, 1px, 2px, 3px, -10px, 10px, 20px, 30px)
Note: the convention in sass is for plain values to automatically be cast to a list containing a single element wherever a list is expected. So any simple value can be passed as an argument list.
In the simplest case, a map can be used to bind values to arguments of the same name as the keys of the map:
@mixin some-mixin($a, $b, $c) { }
$my-args: (a: 1, b: 2, c: 3);
@include some-mixin(...$my-args);
However, just as in the case of positional arguments, you can pass keyword arguments at any place in the argument list, but for this to work we need to fix the semantics of mixing positional and keyword arguments which is currently broken in some edge cases.
Currently in Sass, positional arguments take precendence over keyword arguments. This means that keyword arguments are effectively shuffled to the end of the positional arguments.
This means that the following calls are currently equivalent:
@include some-mixin(1, $b: 2, 3);
@include some-mixin(1, 3, $b: 2);
This is bad. To explain, consider this example:
@mixin some-mixin($a, $b, $c) { }
@include some-mixin(1, $b: 2, 3);
The above example generates an error counter-intuitively because the keywords
are moved to the end, causing 1
to be mapped to $a
, 3
to be mapped to
$b
and then the keyword argument overwrites $b
to have the value of 2
and
$c
to have no value and thus be missing a value causing an error.
So staring in Sass 3.2, the semantics will be changed so that keyword arguments first are assigned to named parameters and then positional arguments are distributed to any unbound parameters. Then the remainder of the arguments are bound to the variable arguments (if any is declared).
This means that the following will all be equivalent and result in $a: 1
, $b: 2
, and $c: 3
:
@include some-mixin($b: 2, 1, 3);
@include some-mixin(1, $b: 2, 3);
@include some-mixin(1, 3, $b: 2);
There are cases where you simply need to wrap an existing mixin or function to augment another one. When you want to share the same argument signature or a very similar one, you can use the argument list variable to pass both positional and keyword arguments.
@mixin replace-text-with-dimensions($image, ...$args) {
@include replace-text($image, $args...);
width: image-width($image);
height: image-height($image);
}
In this case, because the $args
variable is an arglist, it will pass all the arguments
that were received transparently whether they are positional or keywords.
However, let's assume that you want to remove some keyword arguments from being passed through transparently. If you pass the arglist variable to the wrapped mixin, the keyword arguments will be passed along with it. In order to prevent the pass-thru behavior you need to decouple the positional arguments from the keyword arguments:
@mixin some-mixin(...$args) {
// make a new list with just the positional arguments.
$positional: join($args, ());
$keywords: keywords($args);
// manipulate $keywords here
@include similiar-mixin($positional..., $keywords...);
}
Generally liking this proposal.
I'd prefer to have the ellipsis on the same side of the variable for both declaration and calling. Otherwise I suspect it'll be tough to remember which side it goes on where. Even if there's a good mnemonic for it, I'd rather just not make it an issue at all.
For reference, I believe Java puts the ellipsis after the argument (well, after the type, but same idea).
I'm not sure this is better than
is-empty(keywords($shadows))
or whatever.The first
@include
in this example is actually an error. I recently made the validation of keyword/positional args much more restrictive. Keyword args now may only be passed at the end of the argument list. It's also an error to specify an argument both positionally and by keyword. I think this is better than the behavior you're proposing.I don't like this. I'd rather have a heuristic that says that an explicit variable keyword map takes precedence over the implicit map in
$args
.