Functional Wrappers in Sass

Kitty Giraudel
Share

I am not a big fan of having global variables all over the place, no matter the language. In Sass, as in CSS, we have nothing but a global namespace (although latest Sass versions progressively provide more scoping features). It’s like a house with a single room: no matter how hard you try to organize your furniturs, at the end of the day there is still only a single room in the house.

What’s the problem with this? When it comes to Sass, the problem is that variables can overlap. What if you have a variable called $baseline, and a Sass grid framework you use have a $baseline variable as well? Or worst, what if a Sass library you included uses a $_ variable to hold private content required for it to work and you happen to override it yourself with your own stuff? Simple: it breaks.

Coming back to my initial point: I am not a big fan of having variables to configure a Sass system. I shall be more precise and say “having only variables” because, of course, variables are the key when it comes to store data. Meanwhile, it has occurred to me that functional wrappers help a lot.

Functional what…?

You could think of function wrappers as getters and setters in a way that they are functions wrapping a variable declaration or retrieving to add some extra features on top of it: validation, error handling, default values, warnings, and so on.

Isn’t it a pain to do this?, you say. It depends. There is something beautiful in a variable: its simplicity. A variable is obvious: it is a value mapped to a storage key. How could it get any simpler than this? Yet, I think raw variables are not best suited to handle things like library configurations and such.

Actually the choice is yours: either you have a function wrapper that handles everything from the very start (errors, parameter validation, default values…), or you make all of this in every functions and mixins you’ve got. I’ll play clear: I’d rather go the former.

How does it work?

Let’s consider a simple use case. We build a little grid system that supports either left-to-right writing mode or right-to-left. From this configuration, our system defines a $main-direction (either left or right) and an $opposite-direction (the opposite of $main-direction). This is how we would write it with default variables:

// Define the content direction for the whole grid system
$writing-mode: 'LTR' !default;

// ...

// DON'T TOUCH
$main-direction:     if($writing-mode != 'LTR', right, left);
$opposite-direction: if($writing-mode != 'LTR', left, right);

What problems can cause this code? Let’s name a few:

  • $writing-mode is supposed to be either LTR or RTL, yet the user can define it to any value without a problem.
  • If $writing-mode is set to ltr, the grid system will run in right-to-left because LTR != ltr.
  • If $writing-mode is invalid, no error nor warning will be emitted. Silent fail.
  • When re-defining $writing-mode inside another context for any reason, the !global flag should be added yet can be forgotten.

I’m sure we could find a couple of other drawbacks, but those are the major concerns we will deal with in this article.

Building the wrapper

So the idea would be to have a function (or better, a mixin) to set the content direction. It will make sure the value is valid, possibly emit warning/error and finally create the global variable.

/// Define the content direction for the whole grid system
/// @access public
/// @param {String} $value ("LTR") - Content direction, either `RTL` or `LTR`
/// @throw Throws an error if the given value is neither `RTL` nor `LTR`.
/// @output Nothing.
@mixin content-direction($value: "LTR") {
  $value: to-upper-case($value);

  @if index("RTL" "LTR", $value) == null {
    @error "Content direction can be either `LTR` or `RTL` (case-insensitive). "
         + "Given: `#{$value}`. Aborting.";
  }

  $__direction__: $value !global;
}

So what’s going on here? The mixin:

  1. Makes sure the given value is uppercase before checking it against valid strings;
  2. Checks the uppercased given value against both RTL and LTR and emit an error if it’s not any of both;
  3. Dynamically creates a global variable called $__direction__ containing the uppercased given value.

Not only is the variable name quite unlikely to conflict with another variable from the application, but also it won’t ever be fetched manually so who cares about the name? Indeed, we’ll have a little wrapper for this.

/// Return the content direction
/// @access private
/// @throw Throws an error if `content-direction` has not been included yet.
/// @return {String} - Either `RTL` or `LTR`
@function get-content-direction() {
  @if global-variable-exists("__direction__") == false {
    @error "Content direction has not been initialized yet. "
         + "Please include `content-direction($value)` before anything else.";
  }

  @return $__direction__;
}

This function returns $__direction__ if the content-direction mixin has already been included, or throws an error to prompt the user to include content-direction before anything else. You’ll see the point of this function is only to avoid having to repeat the error in our two incoming accessors for the main layout direction and the opposite direction.

Layout directions

When we want to access either the main layout direction or the opposite layout direction, we use any of the following functions:

/// Return the layout direction
/// @access public
/// @require {function} content-direction
/// @return {Position} - Either `left` or `right`
@function get-layout-direction() {
  @return if(get-content-direction() == "LTR", left, right);
}

/// Return the layout opposite direction
/// @access public
/// @require {function} content-direction
/// @return {Position} - Either `left` or `right`
@function get-layout-opposite-direction() {
  @return if(get-content-direction() == "RTL", left, right);
}

The way they work should be quite obvious at this point: they get the content-direction, and return either left or right.

Again, we could have checked $__direction__ directly rather than building a get-content-direction() function however we would not have been able to provide a helpful error message if the user tries to get a layout direction before defining the content direction with content-direction mixin. Instead, the user would have seen:

Undefined variable: “$__direction__”.

Not very helpful, is it? Especially since the user has absolutely no idea what $__direction__ is…

Using it

// Define the content direction for the grid
@include content-direction("RTL");

// Framework stuff
%grid-whatever-tool {
    margin-#{get-layout-direction()}: 1em;
}

Final thoughts

I admit it might seem a little overkill to have 3 functions and 1 mixin to initialize 3 variables, and in most cases I could not agree more with this statement.

However, when providing an API to the end user, I find it more elegant to ask them to include content-direction() with the value they want, catching any exception and providing useful error messages rather than letting Sass throw a random error at some point because they made an honest mistake.

Also, when a system has a couple of options, it gets very easy to wrap them all in a map, and validate any value from this map in the default mixin, resulting in a pretty neat API:

@include set-options((
    'baseline': 42px,
    'debug': false,
    'color': hotpink
));

By the way, this is the way Compass, Susy and some other advanced Sass frameworks went so you can say it’s not such a bad idea after all. ;)

You can play with the code on SassMeister.