Extra Map Functions in Sass

Kitty Giraudel
Kitty Giraudel
Share

Sass maps are awesome. They make a lot of things that were previously impossible, well, possible. For instance, they have a smart way to define dynamic variables. They are also the perfect container for a complex configuration, for a framework or a grid system.

Fortunately for us, Sass provides a handful of functions to manipulate maps. Yet, if you keep working with Sass you might come up with an edge case that is not being covered by the built-in API.

Perhaps you’ll need a way to get a key that is being nested within several maps? What if you want to update this key? Or extend nested maps like $.extend from jQuery? A few problems here and there that can be easily fixed with the functions we’ll inspect today.

Map Deep Get

When you need to access a key that belongs to a map which happens to be nested within another map (or several), you cannot rely on map-get anymore. At least, not as it currently is.

/// Fetch nested keys
/// @param {Map} $map - Map
/// @param {Arglist} $keys - Keys to fetch
/// @return {*}
@function map-deep-get($map, $keys...) {
  @each $key in $keys {
    $map: map-get($map, $key);
  }

  @return $map;
}

Example

$grid-configuration: (
  'columns': 12,
  'layouts': (
    'small': 800px,
    'medium': 1000px,
    'large': 1200px,
  ),
);

// Without `map-deep-get`
$medium: map-get(map-get($grid-configuration, 'layouts'), 'medium');

// With `map-deep-get`
$medium: map-deep-get($grid-configuration, 'layouts', 'medium');

Map Deep Set

As you can see, building a function to get a key from within nested maps is not overly difficult to write. On the other hand, a function to set a value at the end of a key chain is not as easy to build… Because of the complexity, we will not get too deep into the code for this one.

However, we will give the signature a close look because I went a bit crazy with this one. For our function to work, we need 3 things:

  • the map;
  • a key chain, that can contain from a single key to a bazillion;
  • the new value for the last key from the key chain.

We have seen in a previous article why it is better to use an arglist when dealing with an unknown number of arguments, which is exactly what we have here with $keys. The problem is, there can be no extra argument after an arglist, so to fix this point we have two ways to write our function.

Either we can put the new value, then the key chain, but having the new value before what’s getting updated looks a little odd to me.

@function map-deep-set($map, $value, $keys...) {}

Or we only have two actual arguments, and consider the last value from $keys to be our new value.

@function map-deep-set($map, $keys.../*, $value */) {}

This is what I decided to go with.

/// Update a key deeply nested
/// @author Kitty Giraudel
/// @param {Map} $map - Map to update
/// @param {Arglist} $keys - Keys to access to value to update
/// @param {*} $value - New value (last member of `$keys`)
/// @return {Map} - Updated map
@function map-deep-set($map, $keys.../*, $value */) {
  $map-list: ($map,);
  $result: null;

  @if length($keys) == 2 {
    @return map-merge($map, (nth($keys, 1): nth($keys, -1)));
  }

  @for $i from 1 through length($keys) - 2 {
    $map-list: append($map-list, map-get(nth($map-list, -1), nth($keys, $i)));
  }

  @for $i from length($map-list) through 1 {
    $result: map-merge(nth($map-list, $i), (nth($keys, $i): if($i == length($map-list), nth($keys, -1), $result)));
  }

  @return $result;
}

Example

$grid-configuration: (
  'columns': 12,
  'layouts': (
    'small': 800px,
    'medium': 1000px,
    'large': 1200px,
  ),
);

// Without `map-deep-set`
$grid-configuration: map-merge($grid-configuration, map-merge(map-get($grid-configuration, 'layouts'), ('large': 1300px)));

// With `map-deep-set`
$medium: map-deep-set($grid-configuration, 'layouts', 'medium', 1300px);

Map Depth

In the article about debugging a Sass map, we saw how we could build a function to retrieve the maximum depth of a Sass map. Let me post it here again for posterity:

/// Compute the maximum depth of a map
/// @param {Map} $map
/// @return {Number} max depth of `$map`
@function map-depth($map) {
  $level: 1;

  @each $key, $value in $map {
    @if type-of($value) == "map" {
      $level: max(map-depth($value) + 1, $level);
    }
  }

  @return $level;
}

Example

$grid-configuration: (
  'columns': 12,
  'layouts': (
    'small': 800px,
    'medium': 1000px,
    'large': 1200px,
  ),
);

// Maximum depth
$depth: map-depth($grid-configuration);
// -> 2

Map Has Keys

Sass provides a function to check whether a map has a given key. We could extend this function to test if Sass has several keys, what do you say? It is actually extremely easy to do, as you can see.

/// Test if map got all `$keys` at first level
/// @author Kitty Giraudel
/// @param {Map} $map - Map
/// @param {Arglist} $keys - Keys to test
/// @return {Bool}
@function map-has-keys($map, $keys...) {
  @each $key in $keys {
    @if not map-has-key($map, $key) {
      @return false;
    }
  }

  @return true;
}

Example

$grid-configuration: (
  'columns': 12,
  'layouts': (
    'small': 800px,
    'medium': 1000px,
    'large': 1200px,
  ),
);

$depth: map-has-keys($grid-configuration, 'columns', 'layouts');
// -> true

$depth: map-has-keys($grid-configuration, 'columns', 'options');
// -> false

Map Has Nested Keys

Along the same lines, the default map-has-key function cannot be used when dealing with nested maps, as it only goes one level deep. Let’s roll up our sleeves to build a deep map-has-key.

/// Test if map got all `$keys` nested with each others
/// @author Kitty Giraudel
/// @param {Map} $map - Map
/// @param {Arglist} $keys - Keys to test
/// @return {Bool}
@function map-has-nested-keys($map, $keys...) {
  @each $key in $keys {
    @if not map-has-key($map, $key) {
      @return false;
    }
    $map: map-get($map, $key);
  }

  @return true;
}

Example

$grid-configuration: (
  'columns': 12,
  'layouts': (
    'small': 800px,
    'medium': 1000px,
    'large': 1200px,
  ),
);

$depth: map-has-keys($grid-configuration, 'layouts', 'medium');
// -> true

$depth: map-has-keys($grid-configuration, 'layouts', 'huge');
// -> false

Map Zip

This is something I have been wanting to do more than once without having a built-in way to do so: merging two lists to build a map, where the first list would serve as keys, and the second list would serve as values.

In case both lists are not the same length, extra values would be discarded and the function would emit a warning to explain what is going on.

/// An equivalent of `zip` function but for maps.
/// Takes two lists, the first for keys, second for values.
/// @param {List} $keys - Keys for map
/// @param {List} $values - Values for map
/// @return {Map} Freshly created map
/// @see http://sass-lang.com/documentation/Sass/Script/Functions.html#zip-instance_method
@function map-zip($keys, $values) {
  $l-keys: length($keys);
  $l-values: length($values);
  $min: min($l-keys, $l-values);
  $map: ();

  @if $l-keys != $l-values {
    @warn "There are #{$l-keys} key(s) for #{$l-values} value(s) in the map for `map-zip`. "
        + "Resulting map will only have #{$min} pairs.";
  }

  @if $min == 0 {
    @return $map;
  }

  @for $i from 1 through $min {
    $map: map-merge($map, (nth($keys, $i): nth($values, $i)));
  }

  @return $map;
}

Example

$layout-names: 'small', 'medium', 'large', 'huge';
$layout-values: 600px, 900px, 1200px, 1500px;

$breakpoints: map-zip($layout-names, $layout-values);
// -> ('small': 600px, 'medium': 900px, 'large': 1200px, 'huge': 1500px)

Map Extend

You might know that map-merge has two purposes: adding a key to a map, and merge two maps together. As of today, I see that there are possibly two flaws in map-merge:

  • it is not recursive, meaning it does work with nested maps;
  • it only takes two maps, no more.

Surely we can do better, right? Let me introduce the map-extend function, ala jQuery. As for map-deep-set, we use a signature hack to have the $deep parameter as the last value from our $maps chain.

/// jQuery-style extend function
/// About `map-merge()`:
/// * only takes 2 arguments
/// * is not recursive
/// @param {Map} $map - first map
/// @param {ArgList} $maps - other maps
/// @param {Bool} $deep - recursive mode
/// @return {Map}
@function map-extend($map, $maps.../*, $deep */) {
  $last: nth($maps, -1);
  $deep: $last == true;
  $max: if($deep, length($maps) - 1, length($maps));

  // Loop through all maps in $maps...
  @for $i from 1 through $max {
    // Store current map
    $current: nth($maps, $i);

    // If not in deep mode, simply merge current map with map
    @if not $deep {
      $map: map-merge($map, $current);
    } @else {
      // If in deep mode, loop through all tuples in current map
      @each $key, $value in $current {

        // If value is a nested map and same key from map is a nested map as well
        @if type-of($value) == "map" and type-of(map-get($map, $key)) == "map" {
          // Recursive extend
          $value: map-extend(map-get($map, $key), $value, true);
        }

        // Merge current tuple with map
        $map: map-merge($map, ($key: $value));
      }
    }
  }

  @return $map;
}

Example

$grid-configuration-default: (
  'columns': 12,
  'layouts': (
    'small': 800px,
    'medium': 1000px,
    'large': 1200px,
  ),
);

$grid-configuration-custom: (
  'layouts': (
    'large': 1300px,
    'huge': 1500px
  ),
);

$grid-configuration-user: (
  'direction': 'ltr',
  'columns': 16,
  'layouts': (
    'large': 1300px,
    'huge': 1500px
  ),
);

// Not deep
$grid-configuration: map-extend($grid-configuration-default, $grid-configuration-custom, $grid-configuration-user);
// -> ("columns": 16, "layouts": (("large": 1300px, "huge": 1500px)), "direction": "ltr")

// Deep
$grid-configuration: map-extend($grid-configuration-default, $grid-configuration-custom, $grid-configuration-user, true);
// -> ("columns": 16, "layouts": (("small": 800px, "medium": 1000px, "large": 1300px, "huge": 1500px)), "direction": "ltr")

Final thoughts

That’s it folks! Obviously, you will not use them in your everyday stylesheet but once in a while, such functions can come in handy. At least, you’ll know where to find them. ;)

Frequently Asked Questions (FAQs) about Extra Map Functions in Sass

What are the basic map functions in Sass?

Sass provides several built-in map functions that allow you to manipulate and access map data. These include map-get(), map-has-key(), map-keys(), map-values(), map-merge(), and map-remove(). Each of these functions serves a unique purpose. For instance, map-get() is used to retrieve a value from a map based on its key, while map-has-key() checks if a particular key exists within a map.

How can I use the map-get() function in Sass?

The map-get() function in Sass is used to retrieve a value from a map based on its key. The syntax is map-get($map, $key). Here, $map is the map from which you want to retrieve a value, and $key is the key of the value you want to retrieve. If the specified key does not exist in the map, the function returns null.

What is the purpose of the map-has-key() function in Sass?

The map-has-key() function in Sass is used to check if a particular key exists within a map. The syntax is map-has-key($map, $key). Here, $map is the map you want to check, and $key is the key you want to find. If the key exists in the map, the function returns true; otherwise, it returns false.

How can I merge two maps in Sass?

You can merge two maps in Sass using the map-merge() function. The syntax is map-merge($map1, $map2). Here, $map1 and $map2 are the maps you want to merge. The function returns a new map that includes all keys and values from both $map1 and $map2. If the same key exists in both maps, the value from $map2 will be used.

How can I remove a key-value pair from a map in Sass?

You can remove a key-value pair from a map in Sass using the map-remove() function. The syntax is map-remove($map, $key). Here, $map is the map from which you want to remove a key-value pair, and $key is the key of the pair you want to remove. The function returns a new map with the specified key-value pair removed.

How can I list all keys or values in a map in Sass?

You can list all keys in a map in Sass using the map-keys() function, and all values using the map-values() function. The syntax is map-keys($map) and map-values($map), respectively. Here, $map is the map from which you want to list keys or values. Both functions return a list of all keys or values in the map.

Can I use JavaScript Map functions in Sass?

No, you cannot directly use JavaScript Map functions in Sass. Sass has its own set of map functions, which are different from JavaScript Map functions. However, you can use JavaScript in conjunction with Sass through tools like Node-sass, which allows you to compile Sass into CSS using Node.js.

What is the difference between Sass maps and JavaScript objects?

While Sass maps and JavaScript objects are similar in that they both store key-value pairs, they are used in different contexts and have different syntaxes. Sass maps are used in the Sass preprocessor to store and manipulate data that will be compiled into CSS, while JavaScript objects are used in JavaScript code to store and manipulate data at runtime.

Can I nest maps in Sass?

Yes, you can nest maps in Sass, meaning you can have a map as a value within another map. This can be useful for organizing complex data structures. You can access values in nested maps using the map-get() function, by passing the keys in the correct order.

Are there any limitations to using maps in Sass?

While Sass maps are a powerful tool for managing complex data, they do have some limitations. For instance, maps cannot be used as selectors or property names, and they cannot be directly output to CSS. They are also not as performant as lists for large datasets. However, these limitations are generally outweighed by the flexibility and convenience that maps provide.