Deep Get/Set in Maps

Avatar of Kitty Giraudel
Kitty Giraudel on (Updated on )

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.