Stop Arguing So Much with Your Mixins

Mixins are arguably one of the more widely used tools in the Sass language next to the variable. Coming with that, mixins are more apt to be the most abused tool in the language. Mixins are extremely powerful and capable of doing a great number of things, but as we are creating these massive bodies of complex code, it’s easy to find ourselves in a place where we have too many arguments in order to use our mixin. It’s at this point we have started to lose comprehension of what’s happening. The complexity of the tool starts to outweigh its value.

Recently in refactoring a mixin with too many arguments, I began to think of how I could use list-maps to solve this issue.

The classic way

Say you are creating a mixin that is relatively complex in scope and will consume a large number of arguments due to the number of alternative outcomes. We have all done it, I know I sure have. In the following example I am using a button mixin, one of the most frequently written and over-engineered solution after Grids, so the following code shouldn’t seem that crazy to you:

@mixin core-button($color, $background-color, $border-color, $background-hover, $border-color-hover, $background-active, $border-color-active) {
  color: $color;
  background-color: $background-color;
  border: 1px solid $border-color;
  border-radius: 3px;
  cursor: pointer;
  font-family: $type-family-title;

  &:hover,
  &:focus {
    background-color: $background-color-hover;
    border-color: $border-color-hover;
  }
  &:active,
  &[aria-selected="true"] {
    background-color: $background-color-active;
    border-color: $border-color-active;
  }
}

Given the relative simplicity of the mixin, it still takes 7 arguments (yes you can have defaults, but that’s not the point). From here things can only get more complex. The number of times you may need to compare back and forth, between the mixin itself and the use of it, to make sure that you are placing the correct value in the proper argument placement. So, you end up with this:

.foo {
  @include core-button(#ffffff, #ededed, #b2005c, #dbdbdb, #ba0060, #c7c7c7, #a60056);
}

Great, a list of hex values. In the moment, it makes perfect sense, you know the order of things. But next time you come back to this, will you remember what #ba0060 is referencing? Was it $background-hover or $border-color-hover?

Logically, the next step when implementing this would likely be to store the list of values in a variable so you can easily re-use them with other buttons, right? So the process goes as follows … see selector > see mixin > see variable. Search for variable > search for mixin > compare argument list > update and hope you got it right. What usually happens was, “Oh crap, I wanted to update the 4th value, not the 5th!”

Not sure about you, but this has annoyed me for years.

The “options” way

With recently moving a large project to the latest version of Libsass, I have spent a good amount of time refactoring old code. One tool that I am trying to find 1001 uses for is list-maps. If you are not familiar with list-maps, here is an article I wrote over a year ago that can help you get started. Excitingly, it looks like Sass’s list-maps support is going to get even more robust in the future, as the features that are found in sass-maps-plus are on the roadmap to becoming an integrated feature of the official Sass library.

List-maps bring to the table a great way to really manage a variable as a series of key-value pairs. Just like a real programming language! \(^O^)/

Looking back at the mixin above, wouldn’t it be great if we could only have only one argument, a list of options perhaps, and then pass in a list-map of variables within? Good news, we can!

As illustrated in the following example, I have removed the dependency on an ordered list of arguments within the mixin. This options method allows me to use list-maps to have a complex array of named arguments in the form of key/value pairs and not have to deal with the complexities and ambiguity of an ordered list of arguments:

@mixin core-button ($options) {
  ...
}

I’ve seen this before …

Functions and mixins in Sass work much like functions on JavaScript. You can have either a list of ordered arguments where you must follow that specific order to address the needs of the function, or you can use named arguments as well. As shown in the following example, using the . syntax, you can basically make a single argument into an object and parse out the individual values for later use.

// Define function to take one "argument", which is in fact an object:
function fnParseInt( oArg ){
  return parseInt( oArg.number, oArg.radix );
}

// Passing in the object literal you can then call like this:
fnParseInt( { number : 'afy', radix : 36 } );

This JavaScript method and similar methods in Sass have served us really well. But expanding on these ideas is what lead me to consider a single option in a mixin versus passing in a list of individually named arguments. Interestingly enough, and this was pointed out to me by a colleague, this very same model is heavily used in much of Backbone. The concept of options in Backbone is basically a javascript object of key/value pairs that provide data to a method call.

var makeVehicle = function(make, options) {
  var vehicle = {};
  vehicle.make  = make;
  vehicle.model = options.model;
  vehicle.year  = options.year;
  vehicle.value = options.value;
  return vehicle;
};

var car = makeVehicle(
  'Lamborghini', {
    model:'Aventador',
    value:'$397,500',
    year:2015,
  });

var minivan = makeVehicle(
  'Toyota', {
    model:'Sienna',
    value:'$28,700',
    year:2015,
  });

Houston, we have a problem

With the new options style mixin and the desire to use a single argument, a simple list of arguments won’t work. Understanding the JavaScript models and understanding on how we can use list-maps, this is easy enough to address.

In the following example I am creating a standard list-map variable and am using key/value pairs to address variable values.

$core-buttons: (
  color:               #ffffff,
  background-color:    #ededed,
  border-color:        #b2005c,
  background-hover:    #dbdbdb,
  border-color-hover:  #ba0060,
  background-active:   #c7c7c7,
  border-color-active: #a60056
);

I already love how this looks. Now instead of a list like we had before, #ffffff, #ededed, #b2005c ..., or a series of overly complicated variables with a naming convention, we have something that makes sense. It reads much like an object in any other language. We understand the role of each value in that list and that can mean a whole lot when you are managing thousands of lines of code.

Update the signal, improve the receiver

Now that we have a list-map with key/value pairs, we need to update all the variables within the mixin so that we can get them out of the list-map. In the following example you will see that I replaced the more traditional ordered argument/variable pairs with the map-get function and the named variables. IMHO, this is WAY more human readable. It’s clear that we are targeting a single variable which is inherited from the $options argument. Then from that map-get function we are asking for simple values, color or border-color. The mixin itself should remain pretty stable post this set up configuration.

@mixin core-button($options) {
  color: map-get($options, color);
  background-color: map-get($options, background-color);
  border: 1px solid map-get($options, border-color);
  border-radius: 3px;
  cursor: pointer;
  font-family: $type-family-title;

  &:hover,
  &:focus {
    background-color: map-get($options, background-hover);
    border-color: map-get($options, border-color-hover);
  }
  &:active,
  &[aria-selected="true"] {
    background-color: map-get($options, background-active);
    border-color: map-get($options, border-color-active);
  }
}

There is something that I really just love about looking at this code. It’s expressive, it’s clear, it’s direct to the point. What I love even more is how it’s actually used. In the following example you will see a mixin with a single argument, or option, that is able to address multiple concerns:

block {
  @include core-button($core-buttons);
}

One variable to rule them all!

Once all the values are in an easy-to-read list-map variable, we are not beholden to one use case. The variable of $core-buttons can be global so these values are not trapped inside the scope of the mixin as they were before. Say I had a simpler case, like making a quick button? In the following example you will see that I can reference that same list-map and just pull out a few key values that I want to make use of without any extraneous naming conventions like core-button--button-color, or use the mixin and find ways to ignore rules I don’t want, or worse yet … just write in regular CSS!

button {
  color: map-get($core-buttons, color);
  background-color: map-get($core-buttons, background-color);
  border: 1px solid map-get($core-buttons, border-color);
}

Same variable. No copy/paste. No long naming conventions to find the original values from variables. All the values I ever need all scoped correctly within the same concept, or dare I say … object.

Conclusion

Coming to this conclusion and having worked on many Sass projects in my career, I can only say, “If you are not using list-maps, what the hell are you waiting for?

The more I use list-maps, the more I find myself greatly reducing the complexity of my code. I too was victim of variable naming conventions. For me, finding new and creative ways of using list-maps, like reducing the number of arguments within a super-mixin, really just helps to dial in the sanity of my code.

Dale Sande

Dale splits his time between San Francisco where he is a UI Architect for AppDirect and Seattle where he is a Front-End Developer instructor for Code Fellows. He is very involved in the Sass community and a core contributor to SassMeister.

comments powered by Disqus