Update single value in a Sass map

Here is something that really bothered me about maps. Since a map is really a modified Sass list it really still is a value of a single variable. So, in the same way that when you update the value of a non-map variable, this completely updates that variable. The solution I am going to point out may be buried deep inside another toutorial that I didn't read, but here we go. If you already know this, cool! If not, COOL!

Using standard variable w/value

Here is a simple example we can easily understand:

$var: value;

.block {
  value: $var;
}

will give you

.block {
  value: value; }

by updating the value of $var like

$var: another-value;

you get

.block {
  value: another-value; }

Pretty simple, right? This is expected behavior and something we can all agree on. Now let's get to the part that irritates me, updating the value or a variable using a map.

Using a map

Let's update the previous example using a map and go through the same scenario. To do so, let's make a map that has some basic attributes of a block element.

$element: (color: orange, height: 100px, border: 10px);

To use this, of course we need to use the map-get() function so that we can reach inside and grab those values.

.block {
  color: map-get($element, color);
  height: map-get($element, height);
  border: map-get($element, border);
}

Great, now we get the following output,

.block {
  color: orange;
  height: 100px;
  border: 10px; }

Now, let's say that we need to update the value of that color. Running into this need to update values as you progress through the cascade, especially for applying themes to projects and is really common. But, working with maps presents the challenge I stated before that, when I need to update a value in the map, I need to re-enter the WHOLE map value again.

$element: (color: purple, height: 100px, border: 10px);

Just so I can get

.block {
  color: purple;
  height: 100px;
  border: 10px; }

Annoying, am I right? And this is a really simple example. What if you are using maps for things like breakpoints or icons? What I just presented is a really common solution that I have seen on a lot of projects and the other night I was really inclined to find a better way.

A better way

Wanting to solve this in a better way I started playing more with the map-merge function and it's pretty clear that you can merge to map lists together, but I was wondering, "What if I merge a variable with a map with another map that shares a key, but uses a different value?"

Sure enough, there is this example in the API docs:

map-merge(("foo": 1, "bar": 2), ("bar": 3)) => ("foo": 1, "bar": 3)

Ok, so try this. If we start with our initial mapped variable,

$element: (color: orange, height: 100px, border: 10px);

and then update like this,

$update: (color: purple);
$element: map-merge($element, $update);

Looking at the merged map, we would get this,

(color: purple, height: 100px, border: 10px)

This in itself is pretty cool and VERY useful. But looking at this again, a colleague next to me tried something even simpler. Updating multiple values it may be cleaner to crate a new variable and merge the two together, but in this example if you need to change only one value, how about trying this?

$element: map-merge($element, (color: purple));

YES! That totally works! So the full example looks like this:

$element: (color: orange, height: 100px, border: 10px);

.block {
  color: map-get($element, color);
  height: map-get($element, height);
  border: map-get($element, border);
}

$element: map-merge($element, (color: purple));

.block {
  color: map-get($element, color);
  height: map-get($element, height);
  border: map-get($element, border);
}

and out output looks like this:

.block {
  color: orange;
  height: 100px;
  border: 10px;
}

.block {
  color: purple;
  height: 100px;
  border: 10px;
}

I like this, I like this a lot.

One more thing

After reading @hugo's article Extra Map Functions in Sass he introduces a concept for setting a deeply nested value in the map. While his solution appears well done, I have been able to acheive the same thing a little simpler and this fits directly into the concept of this article.

So using Hugo's function (with a name chage)...

@function map-get-deep($map, $keys...) {
  @each $key in $keys {
    $map: map-get($map, $key);
  }

  @return $map;
}

we will use his example of for the grid

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

But lets say that need to change the value of the medium var in that map for an extended use case? I can use the method described above, just need to extend the syntax a little and add more parens.

$grid: map-merge($grid, ('layouts': ('medium': 555px)));

Then I can call in that deep map function and extract the updated value.

.block {
  width: map-get-deep($grid, 'layouts', 'medium');
}

and get

.block {
  width: 555px;
}

This works in both Sass 3.4 and Libsass 3.1. Cool.