Debugging Sass Maps
Maps are great, but maps are not that easy to debug. When you have simple maps, it’s usually quite easy to understand what’s going on, but when you work with huge and/or nested, possibly dynamically updated maps, it can get difficult to keep track of what happens.
…and then, all of a sudden, a bug appears somewhere in your code. Breaking the site you’re developing somehow.
Fear not my friends, I have a possible solution.
What about inspect()
or @debug
?
The other day, web developer Stuart Trann asked how we could debug a map to see what it looks like.
The first thing that came to my mind was the inspect()
function. This function was added at pretty much the same time when maps came out, in order to inspect the content of a map.
You may have noticed that if you try to display a map as a CSS value, you get this message:
(sass: map) isn’t a valid CSS value.
Dang. So that’s why the inspect()
function has been implemented to Sass core. So you can see something like:
test {
inspect: (sass: map);
}
A problem with inspect()
is that it displays the map as a single line, which is far from convenient, especially when you have loads of keys in there. One could think of @debug
, which intends to print some content in the console but that really doesn’t help much since it also keeps it to one line.
Time to build something that helps us folks.
Mixin-ify all the things!
What if we used the fact that CSS is quite neat when it comes to displaying key/value pairs? We could loop on the map, and display the key and the value as a declaration in a dummy selector. Let’s do this!
/// Prints a map as a CSS rule
/// @param {Map} $map
@mixin debug-map($map) {
@at-root {
__properties__ {
@each $key, $value in $map {
#{$key}: $value;
}
}
}
}
Note: we are using __properties__
to make it clear that it’s some internal debugging stuff, kind of like __proto__
in JavaScript.
Not bad. Now, what if we have a nested map inside this one? Sass will fail on the $value
display. So we could either come up with some weird and complex logic, or we could keep it simple and use inspect()
, like this:
/// Prints a map as a CSS rule
/// @param {Map} $map
@mixin debug-map($map) {
@at-root {
__properties__ {
@each $key, $value in $map {
#{$key}: inspect($value);
}
}
}
}
This way, if we encounter a nested map, it will be display as any other value. If we want to debug this nested map now, we should use the debug-map
mixin on this specific value directly.
Adding some extra informations
What if we decided to add some extra information, like the length, the depth or the string representation of the inspected map? Now, that could be helpful, couldn’t it?
To do that, we need to tweak our mixin a bit. Instead of directly outputting a selector, we could print a proprietary @
-directive grouping everything we need, like so:
@mixin debug-map($map) {
@at-root {
@debug-map {
__toString__: inspect($map);
__length__: length($map);
__keys__: map-keys($map);
__properties__ {
@each $key, $value in $map {
#{$key}: inspect($value);
}
}
}
}
}
To compute the depth, we need an extra tool since there is no built-in function for this. It turns out I’ve made such a function a while back but never blogged about it. It’s about time I guess!
/// Compute the maximum depth of a map
/// @param {Map} $map
/// @return {Number} max depth of `$map`
@function depth($map) {
$level: 1;
@each $key, $value in $map {
@if type-of($value) == "map" {
$level: max(depth($value) + 1, $level);
}
}
@return $level;
}
This is a recursive function, looping over each key/value pair from the map until it finds a map, in which case it calls itself and so on. Then, it returns a number: the map’s depth. Let’s update our debug-map
mixin with this information:
@mixin debug-map($map) {
@at-root {
@debug-map {
__toString__: inspect($map);
__length__: length($map);
__depth__: depth($map);
__keys__: map-keys($map);
__properties__ {
@each $key, $value in $map {
#{$key}: inspect($value);
}
}
}
}
}
A last addition I can suggest is printing the type of each value from the map right before its associated key. This could be handy in some cases:
@mixin debug-map($map) {
@at-root {
@debug-map {
__toString__: inspect($map);
__length__: length($map);
__depth__: depth($map);
__keys__: map-keys($map);
__properties__ {
@each $key, $value in $map {
#{'(' + type-of($value) + ') ' + $key}: inspect($value);
}
}
}
}
}
Let’s try this!
Let’s look at the following map for our tests:
$person: (
'name': 'Hugo Giraudel',
'location': 'France',
'age': 22,
'glasses': true,
'kids': null,
'skills': ('HTML', 'CSS', 'Sass', 'JavaScript'),
'languages': (
'French': 'native',
'English': 'fluent',
'Spanish': 'notions'
)
);
And now, let’s debug it!
@include debug-map($person);
Once compiled it would yield this result:
@debug-map {
__toString__: ("name": "Hugo Giraudel", "location": "France", "age": 22, "glasses": true, "has-kids": false, "skills": ("HTML", "CSS", "Sass", "JavaScript"), "languages": (("French": "native", "English": "fluent", "Spanish": "notions")));
__length__: 7;
__depth__: 2;
__keys__: "name", "location", "age", "glasses", "has-kids", "skills", "languages";
__properties__ {
(string) name: "Hugo Giraudel";
(string) location: "France";
(number) age: 22;
(bool) glasses: true;
(null) kids: false;
(list) skills: "HTML", "CSS", "Sass", "JavaScript";
(map) languages: ("French": "native", "English": "fluent", "Spanish": "notions");
}
}
Final thoughts
There you go folks. I hope that this might come in handy if you ever struggle with maps. Feel free to improve or add extra features to this little mixin. My goal was to keep it as simple as it can be and not to over-crowd it too much. Even the map depth might seem a little overkill in most cases, but you know… Nice addition!