Exploring Maps In Sass 3.3(Part 4): Improving the map-get function

Micah Godbolt, Developer
#Mapping | Posted

This is part 4 of my blog series about Maps in Sass 3.3.  Check out part 1, part 2, and part 3 to get the full story! One thing I admit, while I transition my brain from variables to maps, is that the map syntax is a bit more verbose. It doesn't take long to notice that $my-variable is quite a few less letters than map-get($my-map, my-key). Therefore, since we have accepted this extra layer of complexity, we might as well embrace it, and get as much value out of a function call as possible.

Color-get is the new map-get($colors,...)

Below is a function I’m calling color-get (just like map-get, but for colors). The purpose of this function is to make retrieving a value easier and more flexible.

  1. $site-colors: (
  2. primary: red,
  3. secondary: yellow,
  4. tertiary: green,
  5. );
  6.  
  7. ////
  8. // @function color-get($key, $opacity: 1, $lighten: 0, $map: $site-colors)
  9. // $key: key to be retrieved from the map
  10. // $opacity: A number between 0 and 1. Defaults to 1
  11. // $lighten: A percentage between -100% and 100%. Defaults to 0
  12. // $map: the map that our $key will be pulled from. Defaults to $site-colors, rarely changed
  13. ////
  14.  
  15. @function color-get($key, $opacity: 1, $lighten: 0, $map: $site-colors) {
  16. $value: map-get($map, $key);
  17. $value: rgba($value, $opacity);
  18. $value: call(if($lighten >= 0, lighten, darken), $value, abs($lighten));
  19. @return $value
  20. }
  21.  
  22. div {
  23. color: color-get(primary);
  24. background-color: color-get(secondary, .5, -10%);
  25. }

Simplified process, better syntax

With color-get, we have simplified the process of getting a color by providing a default map to pull from. In addition, we are providing an opportunity to change the opacity of the color, lighten or darken it, or even both! Sure you could just wrap your returned color in a rgba function, and then wrap that in a lighten or darken function, but look at the difference in syntax:

  1. background-color: darken(rgba(map-get($site-colors, secondary),.5)10%);
  2. vs
  3. background-color: color-get(secondary, .5, -10%);

The color-get is obviously easier to read and less prone to typos, but the function also makes it easier to add or remove modifiers, which makes using color-get inside other functions or mixins much easier.

Testing and Error Reporting

In this next example I’ll create a function for retrieving numerical values from a map, called number-get. I’ll add even more value to the function by introducing testing and error reporting.

  1. $nav: (
  2. margin-top: 30px,
  3. font-size: 1.25em,
  4. );
  5.  
  6. ////
  7. // @function number-get($map, $key, $user-unit: em, $context: 16px)
  8. // $map: the map to retrieve the value from
  9. // $key: key to be retrieved from the map
  10. // $user-unit: the numerical unit the user is asking for. Default is em
  11. // $context: the pixel context used to calculate ems (could set default to global var)
  12. ////
  13.  
  14. @function number-get($map, $key, $user-unit: em, $context: 16px) {
  15. $value: map-get($map, $key);
  16. $map-unit: unit($value);
  17. @if (type-of($value) != number) {
  18. @warn "You called number-get on a value that was not a number";
  19. }
  20. @else if ($map-unit == '') {
  21. @warn "You called number-get on a unitless value, please add a unit to #{$key} ";
  22. }
  23. @else if not (index(px em, $user-unit)) {
  24. @warn "User-unit must be in px or em, not #{$user-unit}";
  25. }
  26. @else if (unit($context) != "px") {
  27. @warn "Context must be supplied in pixels";
  28. }
  29. @else if $map-unit != $user-unit {
  30. $value: if($map-unit == px, $value/$context * 1em, $value / 1em * $context);
  31. }
  32.  
  33. @return $value;
  34. }
  35.  
  36. nav {
  37. font-size: number-get($nav, font-size, $user-unit: px);
  38. }

Using number-get()

Now, if we want to get our nav font-size in pixels instead of ems, we can simply change the $user-unit to px, and we get 20px returned, instead of 1.25em.

Catching and Throwing

So what happens when things go wrong? What if we end up pulling a non numerical value from our map, or we  pull a value, but it is missing its unit (px or em). What if we pass ‘points’ as the unit we want returned, or a context that isn’t in pixels? Fortunately Sass allows us to easily test for these conditions and provides a @warn function that allows us to report back to the user when things go wrong.

Try asking for number-get($nav, font-size, point) , and you’ll get a warning of "point is not a valid value for $user-unit".

  1. ...
  2. @else if not (index(px em, $user-unit)) {
  3. @warn "User-unit must be in px or em, not #{$user-unit}";
  4. }
  5. ...

Try to pass a context of 2em and you’ll receive a warning of "Context must be supplied in pixels".

  1. ...
  2. @else if (unit($context) != "px") {
  3. @warn "Context must be supplied in pixels";
  4. }
  5. ...

There are several other checks and warnings you could include, but this is a good start.

Going Deeper

Next, we are going to get our margin-top value, but we want it in ems, not pixels. here we have a small problem, as our context has changed. 16px is no longer the font-size of the nav element, it’s now 20px.

Remember: target ÷ context = result

To solve this problem we need to pass in a $context of the nav’s font size. Since the font-size is currently an em value inside our map, we’ll need to call number-get from within a number-get function. Number-getception!

  1. margin-top: number-get(
  2. $nav,
  3. margin-top,
  4. em,
  5. $context: number-get($nav, font-size, px)
  6. );

The function will then properly determine that the margin-top needs to be set at 30px / 20px, or 1.5em.

This is a pretty edge case example, and a good reason not to mix your units like this, but you can see how using number-get allows us to mimic the original functionality of map-get, while have better control over the condition of the output.

Wrapping it all up

Custom map-get functions are very powerful, and allow complete control over the values pulled from a map. Just be sure to document all of your functions thoroughly, test your values as they go through your function, and using @warn to help people understand how they might have misused the functions you’ve created.

 

Micah Godbolt

Developer