When working on complex Sass architectures, it is not uncommon to use Sass maps to maintain configuration and options. From time to time, you’ll see maps within maps (possibly on several levels) like this one from o-grid:
$o-grid-default-config: (
columns: 12,
gutter: 10px,
min-width: 240px,
max-width: 1330px,
layouts: (
S: 370px, // ≥20px columns
M: 610px, // ≥40px columns
L: 850px, // ≥60px columns
XL: 1090px // ≥80px columns
),
fluid: true,
debug: false,
fixed-layout: M,
enhanced-experience: true
);
The problem with such maps is that it is not easy to get and set values from the nested tree. This is definitely something you want to hide within functions in order to avoid having to do it manually every time.
Deep get
Actually, building a function to fetch deeply nested values from a map is very easy.
/// Map deep get
/// @author Kitty Giraudel
/// @access public
/// @param {Map} $map - Map
/// @param {Arglist} $keys - Key chain
/// @return {*} - Desired value
@function map-deep-get($map, $keys...) {
@each $key in $keys {
$map: map-get($map, $key);
}
@return $map;
}
For instance, if we want to get the value associated to the M
layout from our configuration map, it is as easy as:
$m-breakpoint: map-deep-get($o-grid-default-config, "layouts", "M");
// 610px
Note that quotes around strings are optional. We only add them for readability concerns.
Deep set
On the other hand, building a function to set a deeply nested key can be very tedious.
/// Deep set function to set a value in nested maps
/// @author Kitty Giraudel
/// @access public
/// @param {Map} $map - Map
/// @param {List} $keys - Key chaine
/// @param {*} $value - Value to assign
/// @return {Map}
@function map-deep-set($map, $keys, $value) {
$maps: ($map,);
$result: null;
// If the last key is a map already
// Warn the user we will be overriding it with $value
@if type-of(nth($keys, -1)) == "map" {
@warn "The last key you specified is a map; it will be overrided with `#{$value}`.";
}
// If $keys is a single key
// Just merge and return
@if length($keys) == 1 {
@return map-merge($map, ($keys: $value));
}
// Loop from the first to the second to last key from $keys
// Store the associated map to this key in the $maps list
// If the key doesn't exist, throw an error
@for $i from 1 through length($keys) - 1 {
$current-key: nth($keys, $i);
$current-map: nth($maps, -1);
$current-get: map-get($current-map, $current-key);
@if $current-get == null {
@error "Key `#{$key}` doesn't exist at current level in map.";
}
$maps: append($maps, $current-get);
}
// Loop from the last map to the first one
// Merge it with the previous one
@for $i from length($maps) through 1 {
$current-map: nth($maps, $i);
$current-key: nth($keys, $i);
$current-val: if($i == length($maps), $value, $result);
$result: map-merge($current-map, ($current-key: $current-val));
}
// Return result
@return $result;
}
Now if we want to update the value associated to the M
layout from our configuration map, we can do:
$o-grid-default-config: map-deep-set($o-grid-default-config, "layouts" "M", 650px);
Extra resources
The above function is not the only solution to this problem.
The Sassy-Maps library also provides map-deep-set
and map-deep-get
functions. Along the same lines, Kitty Giraudel also has written a jQuery-style extend
function to make the built-in map-merge
recursive and able to merge more than 2 maps at once.
Typo:
should be
Cool snippet though!
-From times to times
Should actually be:
From time to time….
Huge timesaver, thank you Hugo!
Usefull as usual!, thank you!
there is even a map-merge-deep() … check out the PR in the Sassy-Maps project. Works like a charm in my projects. https://github.com/at-import/Sassy-Maps/issues/9
That’s a good start! I took the getter and created a namespace-like version.
First, I created a function that splits the string by periods into keys, then gets the value from the provided map.
You could then create helper methods to access various configuration maps to abstract away the underlying map, like the following:
@Travis That’s really helpful, thanks!
Sory for styles (I don’t know how make pretty).
So my variant “get” function for use in any cases.
@function get($map, $paths...){
$allParts: ();
@each $path in $paths {
$pathParts: str-split($path, '.');
@each $pathPart in $pathParts {
$allParts: append($allParts, $pathPart);
}
}
@each $key in $allParts {
$map: map-get($map, $key);
}
@return $map;
}
use:
get($buttons, 'foo.bar.baz');
get($buttons, 'foo', 'bar', 'baz');
get($buttons, foo, bar, baz);
get($buttons, 'foo.bar', 'baz');
get($buttons, 'foo.bar', baz);
get($buttons, 'foo', 'bar.baz');
I’m having little trouble with map-deep-get. In your example, the function gets an individual specific value. But when I try this in codepen, I get an error. It seems to be getting all the keys ad values. https://codepen.io/aalokt89/pen/NLzvpL
Nice function but it assumes the keys all exist which may not be the case. I tweaked it slightly to prevent this and return “null” if any key in the chain isn’t found:
@marc Niiice Thank you! I need this as a conditional to check for existence of keys too