Deep Get/Set in Maps

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 Hugo 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 Hugo 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, Hugo 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.

Comments

  1. User Avatar
    Anon
    Permalink to comment#

    Typo:

    From times ot times

    should be

    From times to times

    Cool snippet though!

  2. User Avatar
    anon
    Permalink to comment#

    -From times to times
    Should actually be:
    From time to time….

  3. User Avatar
    Lucas

    Huge timesaver, thank you Hugo!

  4. User Avatar
    Diego
    Permalink to comment#

    Usefull as usual!, thank you!

  5. User Avatar
    4aficiona2
    Permalink to comment#

    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

  6. User Avatar
    Travis

    That’s a good start! I took the getter and created a namespace-like version.

    .a { font-size: config('bar.size.l'); } 
    

    First, I created a function that splits the string by periods into keys, then gets the value from the provided map.

    @function ns($map, $path) {
        $keys: ();
        $separator: '.';
        $index : str-index($path, $separator);
    
        @while $index != null {
            $item: str-slice($path, 1, $index - 1);
            $keys: append($keys, $item);
            $path: str-slice($path, $index + 1);
            $index : str-index($path, $separator);
        }
    
        $keys: append($keys, $path);
    
        @each $key in $keys {
            $map: map-get($map, $key);
        }
    
        @return $map;
    }
    

    You could then create helper methods to access various configuration maps to abstract away the underlying map, like the following:

    $config: (
        foo: 'foo',
        bar: (
            baz: 'bar.baz',
            size: (
                m: 14px,
                l: 18px,
                xl: 270px
            )
        )
    );
    
    
    @function config($path) {
        @return ns($config, $path);
    }
    

Submit a Comment

Posting Code

You may write comments in Markdown. This makes code easy to post, as you can write inline code like `<div>this</div>` or multiline blocks of code in triple backtick fences (```) with double new lines before and after.

Code of Conduct

Absolutely anyone is welcome to submit a comment here. But not all comments will be posted. Think of it like writing a letter to the editor. All submitted comments will be read, but not all published. Published comments will be on-topic, helpful, and further the discussion or debate.

Want to tell us something privately?

Feel free to use our contact form. That's a great place to let us know about typos or anything off-topic.

icon-anchoricon-closeicon-emailicon-linkicon-logo-staricon-menuicon-nav-guideicon-searchicon-staricon-tag