Search code examples
csssassmixins

SASS mixin - concatenate/collapse to a single property?


Im trying to write a simple mixin that will generate some cross-browser code for the passed property, but if you call it multiple times I want it to somehow append the new values to the existing property rule.

For example:

=foo($foo)
   foo: "#{$foo}"

.test
   +foo( test 1 )
   +foo( test 2 )

Will generate

.test {
   foo: "test 1";
   foo: "test 2";
}

But what Im trying to get it to generate is:

.test {
   foo: "test 1, test 2";
}

I know I could just do +foo(test 1, test 2), but sometimes i might have many arguments and since the indentation based SASS syntax doesnt let you split mixin arguments over multiple lines (sadly), I want a cleaner way to use this mixin without having tons of arguments crammed in on 1 line


Solution

  • Sass does not handle what you are looking for – but you can workaround it using maps, global flags and include wrappers. Why I would consider the following an antipattern.

    Demo on codepen

    Note! the following may need some elaboration but for now I'll just throw in some SCSS (for the broader audience) – I'm sure you can convert it to Sass

    Global variables

    First we create a set of global variables to keep states and values across includes.

    $render-map:();  //  map to hold key value pairs for later render
    $render: false;  //  render flag if true we print out render-map
    $concat: false;  //  concat flag to trigger value concatenation 
    

    Render mixin

    To handle the tedious work of keeping track of what to render we create a multi usage render mixin. The mixin can be used inside other mixins to set keyvalues and inside selectors to render unique properties. We'll later create a small mixin to handle value concatenation as this is the less common use case.

    @mixin render($args...){
        //  no arguments passed and not in the state of rendering
        //  1) switch to rendering state 
        //  2) include content (nested included)
        //  3) render render-map content
        //  4) before exit disable render state 
        //  5) empty render-map
        @if length($args) == 0 and not $render {
            $render: true !global; // 1
            @content;              // 2   
            @each $key, $value in $render-map { #{$key}:$value; } // 3
            $render: false  !global; // 4
            $render-map: () !global; // 5
        } 
    
        //  if arguments are passed we loop through keywords to build our render-map  
        //  the keyword key is the same as the passed variable name without the `$`
        //  e.g.   @include render($margin-left: 10px) becomes  margin-left: 10px
        //  1) get keywords
        //  2) loop through keywords
        //  3) look for existing render-map values or use empty list
        //  4) in case we have a concat flag concatinate render-map value
        //  5) in case we don't have a concat flag we overwrite render-map value
        //  6) add key value pair to render-map
        @else {
            $keywords: keywords($args);        // 1
            @each $key, $value in $keywords {  // 2
                $map-value: map-get($render-map, $key) or (); // 3
                @if $concat { $map-value: if($map-value, append($map-value, $value, comma), $value); } // 4
                @else { $map-value: if($value, $value, $map-value); } // 5
                $render-map: map-merge($render-map, ($key: $map-value)) !global; // 6
            }        
        }
    }
    

    Render Concat

    To handle value concatenation we create a wrapper mixin for our render mixin handling the global concat flag.

    Note render-concat is only used for setting key/value pairs inside mixins – why it does not take a content block.

    @mixin render-concat($args...){ 
        $concat: true !global;     // set global concat flag for render mixin
        @include render($args...); // pass args on to the render mixin
        $concat: false !global;    // reset global concat flag
    }  
    

    Usage

    @mixin foo($value){
        //  add the passed value to the `foo` key ($ is stripped) of the render-map.    
        @include render-concat($foo: $value);
    }
    
    
    .test {
        //  in order to render our render-map content we wrap our includes
        //  inside a @include render (without any arguments).
        //  note the quoted strings to prevent sass from thinking we are passing lists
        @include render {
            @include foo('test 1');
            @include foo('test 2');
            @include foo('test 3');
        }
    }
    

    Output

    .test {
      foo: "test 1", "test 2", "test 3";
    }
    

    As said be very careful using this... you could easily get unexpected output.