Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support nested maps in sass:map functions #1739

Closed
4 of 5 tasks
blackfalcon opened this issue Jun 4, 2015 · 36 comments
Closed
4 of 5 tasks

Support nested maps in sass:map functions #1739

blackfalcon opened this issue Jun 4, 2015 · 36 comments
Labels
enhancement New feature or request planned We would like to add this feature at some point specs written Specs have been written for the feature and at least one implementation passes them

Comments

@blackfalcon
Copy link

blackfalcon commented Jun 4, 2015

Edited by @nex3


@lunelson project sass-list-maps has support for nested list maps, using the map-get-z function.
https://github.com/lunelson/sass-list-maps

The following example says it all:

$list-map-z: (
  alpha (
    beta (
      gamma 3
    )
  )
);

.demo {
  out: map-get-z($list-map-z, alpha); // -> ( beta ( gamma 3 ) )
  out: map-get-z($list-map-z, alpha, beta); // -> ( gamma 3 )
  out: map-get-z($list-map-z, alpha, beta, gamma); // -> 3
}

FACT: Up to a point, even when using maps extended naming conventions come into play. Using map-get-z we can actually use structure to categorize versus naming conventions. I vote for structure every time.

$carousel: (
  color (
    header (
      border gray,
    )
  )
);

$side-nav: (
  bg gray,
  color (
    hover (
      search yellow,
      home red,
      filter blue
    )
  )
);

block {
  color: map-get-z($side-nav, color, hover, filter);
}

block {
  border-color: map-get-z($carousel, color, header, border);
}

I don't mind using the plug-in, but I think that this is a feature that Sass should support natively.

@nex3
Copy link
Contributor

nex3 commented Jun 5, 2015

I'm not opposed to allowing map-get() to take multiple keys to do nested retrieval.

Off-topic, but:

$side-nav: (
  bg gray,
  color (
    hover (
      search yellow,
      home red,
      filter blue
    )
  )
);

This isn't a map, it's a (nested) list of pairs. It works with map functions for compatibility reasons, but (in addition to being harder to read than a real map) it's likely to be much less efficient.

@nex3 nex3 added enhancement New feature or request planned We would like to add this feature at some point labels Jun 5, 2015
@lunelson
Copy link

lunelson commented Jun 5, 2015

@nex3 yes @blackfalcon is referring to my non-native library which used lists of pairs; I have a newer version which implements the same argument patterns for map-get-z() and map-merge-z() with native maps—https://github.com/lunelson/sass-maps-plus —and so what do you think of comma-separated keys syntax for map-merge? Notably, in my implementation, providing a non-map as the final argument causes causes it to work like a map-set(), and empty keys/nests are created as necessary. So you could write merges in one of two ways:

$map: ();
$map: merge($map, alpha, beta, (gamma: 5));

// n.b. equivalent to above
// $map: merge($map, alpha, beta, gamma, 5); 

// result
// $map: (alpha: (beta: (gamma: 5)));

@nex3
Copy link
Contributor

nex3 commented Jun 5, 2015

@lunelson 👍

@ArmorDarks
Copy link

Sorry for offtopic again, but I always wondered why Sass didn't use more understandable arrays ([]) instead of lists and objects (dicts) ({}) instead of maps, it would be much easier to understand $side-nav example:

$side-nav: [
  bg gray,
  color: {
    hover: {
      search: yellow,
      home: red,
      filter: blue
    }
  }
];

Regarding $map: merge($map, alpha, beta, (gamma: 5)); — I have a feeling that it will result in issue, which natively has JavaScript, where you're unable to use keywords to define function's arguments.

In other words, such approach would be more flexible, since dosn't require strict order of arguments:

// bulletproof way, since even if to API of `merge` will be added new methods,
// you would have more chances to be on safe side
$map: merge($map, $path: (alpha, beta, gamma), $set: true);

// or when you want shorter way and don't care much about any changes
$map: merge($myMap, (alpha, beta, gamma), true);

In other words, $path should contain list of keys for lookup.

I'm not enough experienced programmer to say, but as far as I know most languages trying to avoid relying on providing of endless arguments unless it's 100% guaranteed that there won't be no new methods for function in future. And, since nobody can foresee future...

Anyway, I definitely vote for native support of map-get-z-like function 👍

@lunelson
Copy link

lunelson commented Jun 8, 2015

@ArmorDarks The upside of this (so-called) map-get-z() argument syntax is that it can also be a simple map-get(): that is, it works as either nested or non-nested depending on the number of arguments provided. So this proposal is not really for a new map-get-z() function but rather to consider an extension to the utility of the existing map-get(): nothing breaks, nothing changes; but if you provide more arguments they would be assumed to be keys at which to address nested maps. The idea was also, simply, that comma-separating keys reads in a similar way, to the way you address objects in javascript or other languages, i.e. dot notation.

WRT your other point, I'm sure this is because curly braces and square braces have very specific meanings in CSS and they wish to avoid confusion. Sass has always restricted itself to syntax styles that seem like part of CSS, and round braces are the only kind seen in vanilla CSS rules (e.g. css transforms etc.)

@KittyGiraudel
Copy link

I'm not opposed to allowing map-get() to take multiple keys to do nested retrieval.

Now that's good news!

Too, I don't see a good reason to introduce map-get-z(..) (or map-deep-get(..), no matter ho you all it). Although, it would be nice to see map-get(..) taking an unknown number of keys, like map-remove(..) does already. This won't break any current code and would implement a new handy feature without introducing yet another function.

@chriseppstein
Copy link

If we're going to have a deep version of map-get I'd like to see a corresponding version of map-merge.

@lunelson
Copy link

@chriseppstein what's your opinion on my comment above?

@ArmorDarks
Copy link

Maybe issue #1349 should be reviewed once more.

In case it will be implemented, you won't need endless arguments for map-get(..), since you would be able to use dot notation in plain form, or inside map-get(..) like map-get($myMap, alpha.beta.gamma).

However, ability to provide map-get(..) with endless arguments (no matter how exactly it will be implemented, via endless arguments, or $path argument with list of keys) would be useful anyway, since there maybe more complex situations, when one of keys would be presented as another map. As far as I know, usually dot and brackets notations doesn't allow to traverse through such complex maps.

@KittyGiraudel
Copy link

As much as I'm used to it, I'm pretty sure dot notation would be a bad idea in the current Sass echosystem.

@ArmorDarks
Copy link

@hugogiraudel Can you share with us your opinion why is it so, in mentioned issue above? Thanks in advance

@davidkpiano
Copy link

@ArmorDarks contrived example: (cc @hugogiraudel)

This is legal:

foo.bar {
  $map: (
    nth(&, 1): 'baz',
    foo: (bar: 'qux')
  );
  $value: map-get($map, nth(&, 1)); // essentially map-get($map, foo.bar);

  test: inspect($value); // => 'baz'
}

However, map-get($map, foo.bar) is (currently) not legal syntax. If it were, which value should it refer to? 'baz' or 'qux'? There is ambiguity, so dot notation would be a bad idea because of the nature of CSS selectors and the necessity for Sass to play nicely with them.

@KittyGiraudel
Copy link

@ArmorDarks The short answer is that CSS (and thus Sass) is function based. If I am not mistaken, this is the main reason why Sass decided to go with functional notation instead of dot notation. Introducing the dot would not only be the source of hard to track bugs like the one explained by @davidkpiano, but also disrupt the global harmony.

@ArmorDarks
Copy link

@hugogiraudel hm, can't agree with it. JavaScript is object-based, with hard rely on functions too. So, it shouldn't use dot notation too, since two ways to do same thing would make things disharmoned? Same for Python, etc.

Not to mention, that JS has dot notation, brackets notation and special functions for getting values from ES6 maps (not objects). And you can write your own function to get values from objects too. Which makes about 4 ways to do, from first glance, same thing, but each approach was introduced to target specific purposes.

@davidkpiano Thanks for your input. Though, I think that discussion should go into mentioned issue above, since we're diving into offtopic deeper.

Since you replied here, I'm probably forced to reply here too.

Regarding your example — there is no error here. With map-get($map, nth(&, 1)); you get baz, because you're refering to function which is key. With map-get($map, foo.bar) you will get qux. since you're traversing through existing keys, which are foo and bar.

map-get($map, foo.bar) is totally == to map-get($map, foo, bar) if endless arguments for map-get() would be implemented ever. So far I don't see issues with it. Maybe I'm missing something.


Once again, I'm sorry for post, that raised another discussion here, which in fact should be discussed here #1349.

Let's back to our current issue. I doubt that someone will argue with fact, that all would only benefit from having map-get() and map-merge() which will accept endless number of arguments.

To summarize, I think that there were only few little things that were missed:

Notably, in my implementation, providing a non-map as the final argument causes causes it to work like a map-set(), and empty keys/nests are created as necessary. So you could write merges in one of two ways:

by @lunelson #1739 (comment)

And my proposal to use $path argument with list inside instead of endless arguments #1739 (comment)

Regarding my proposal — it seems that Sass community more appreciate endless arguments approach, and using of $path argument with list seems to be a bit against current approaches of Sass.

@tjbenton
Copy link

@ArmorDarks I agree with you about list/arrays and maps/objects in your comment. I would have liked to see these features implemented in the same way that js and other major languages. It would make complex things easier to read because you can actually see the difference between getting a value from a map or list and calling a function with that value, and see the difference between () for order of operations, and () that are just getting a value to use inside of the order of operations. I think that the () are over used in sass and at times has cause a lot of confusion when developing functions/mixins and using lists inside of maps that call other functions that have (). I've see )))))) at the end of a line and I have no idea what ) is doing what without taking a few min to break it down into multiple lines. When it could be simplified to be ))) if dot/bracket notation was implemented.

In less than 3 seconds can you determine this ) is tied to in the example below? Order of operations, map a function, part of abs function?

$promo-bg: map-merge($promo-bg, (
 origin-fix: ((100% + abs(map-get(map-get($promo-bg, foo), offset))) / (map-get(map-get($promo-bg, foo), width) / (100% + abs(map-get(map-get($promo-bg, foo), offset))))) / 2
----------------------------------------------------------------------------------------------------------------------------------------------------------------------^
));

Here's the same test with dot notation.

$promo-bg.foo.orgin-fix: ((100% + abs($promo-bg.foo.offset)) / ($promo-bg.foo.width / (100% + abs($promo-bg.foo.offset)))) / 2;
----------------------------------------------------------------------------------------------------------------------^

@davidkpiano
Copy link

Keep in mind, for lexical parsing reasons (and potential complexity), that this is legal:

$foo\.bar: 'baz';

.foo {
  test: $foo\.bar; // => "baz"
}

@ArmorDarks
Copy link

@tjbenton thanks for the support. I totally agree with you.

More of it, I was always confused, why Sass preferred to use () for maps. Some people pointing, that it's because CSS rules already using {}.

But am I the only person, that seeing, that in fact CSS's rules are objects itself, but only with ; instead of , as delimiter? You can even drop last ; in ruleset, just like in JavaScript's object, or Python's dict.

For the sake of illustration, object-like ruleset in CSS:

cssObject {
  width: 1px;
  color: #000;
}

and JavaScript object:

var cssObject = {
  width: 1px,
  color: #000
}

I hope everybody can see how they are close. Why would we need to invent new syntax with (), which already were used by functions and lists (arrays), while we already had objects in fact?

We can open issue about {} instead for objects and [] for maps, but I don't believe that it will be changed ever... though, I must admit, it's possible to add {} and {} as optional notation (it won't break anything so far), so that well-pointed overused () could be used by legacy projects and would be dropped in future, after few major versions.

@davidkpiano
Copy link

@ArmorDarks Keep in mind that CSS rulesets are not exactly objects like JavaScript objects. This is legal in CSS but not legal as an object/dict/map:

.ruleset {
  font-size: 24px;
  font-size: 2rem; // illegal in JS
}

@lunelson
Copy link

I would imagine the reason that both dot-notation and curly-brace-object-notation have not been used for Sass syntax, despite their well known meaning in other languages like JS, is that Sass' syntax has always been modelled to look like CSS and to not conflict with existing CSS syntactical patterns. Thus curly braces are exclusively used to delimit CSS declarations and dots—adjacent to strings—are exclusively used to indicate class selectors. This might seem unnecessary or perverse but in my observation keeping the language clear and accessible to new users has always been a high priority for Sass, and I believe it's correct

@ArmorDarks
Copy link

@davidkpiano I'm talking not about literal similarity, but about same conceptual approach. CSS's rulestes are objects by it's nature, but with slightly another syntax

@lunelson I can't agree with that. Introducing of () for list and in same time for maps (for two different entities) nor make syntax looks like CSS (because in CSS () used only for inner values, like url(), not for associated values), nor doesn't sound like solution to that problem, not to mention that it doesn't increase accessibility for new users, because () for arrays and objects are very uncommon in other languages. On top of it, mentioned by @tjbenton issue with (((((()))))) in functions with maps, lists etc.

@tjbenton
Copy link

While @davidkpiano is correct

CSS rule sets are not exactly objects like JavaScript objects
But it looks very similar, and when new people learn css with a JS background it's how they think of the styles because they think of a rule set as an Object. If you run typeof document.styleSheets[0].cssRules[0].style in a browser you can see that JS stores the rules in an Object. So it's not a far leap to think of them in that way.

@lunelson Adding dot notation and curly-brace object notation wouldn't conflict with existing CSS syntactical patterns. It would actually be more inline with CSS patterns that what is currently implemented.

Dots

  • CSS uses dots to increase the specificity level
  • Dot notation uses dots to take you deeper into a map

Brackets

  • CSS uses brackets to allow you to select attributes and that increases the specificity level
  • Bracket notation is used to select items via a variable, string, or because the key is a list or map, and that takes you one level deeper in the map.

Rule set/structure

  • A CSS rule set is comprised of a selector followed by a declaration block of properties and values; the rule set applies the declarations listed in the declaration block to all elements matched by the selector.
    scss .foo{ border: { top: { right: { radius: 6px; }; }; }; }
    • A curly brace structure is comprised of a variable(key, or in an array) followed by a declaration block of keys and values; the structure is associated with the variable/array/key
$foo: {
 bar: [
  {
   name: Lorem,
   theme: a,
  },
  {
   name: Lorem,
   theme: c,
  }
],
 baz: {
  qux: "garply"
 }
};

Using [] for lists instead () is the only one that doesn't follow css. But you can't honestly, tell me that using () follows css either. You can try argue that linear-gradient(#fff 0%, #000 100%) is the reason that () were used and why you can have space and comma delimited lists. But I don't think that's a valid argument because in CSS linear-gradient is a CSS function and the lists inside of it are actually an arguments list. You could also argue that the reason why you can have both spaces and commas are because that is how CSS selectors work, but css selectors don't have () in them unless it includes a pseudo selector like :matches(section, article, aside, nav) which is acting like a function.

As for the parsing of it, I don't think that it would be that difficult because the only dot/bracket notation has to be tied to a variable. When the variable is defined it has to be followed by a : and you can't end a css selector with a : because it's not a valid selector. Every SASS variable has to start with a $. Given that information it's easy to tell the difference between a sass variable and a css selector. Even when used in interpolation it would still be easy to see the difference

$foo: {
 bar: ".baz.qux.quux"
}; // structure
.foo{}; // css selector
#{$foo.bar}[class*="awesome"]{}

While I would love to see sass change to use {}, and [] I don't see sass changing it or even having it be an optional syntax while the other is phased out, and if by some miracle it was approved, It would take a while for it into production.

@ArmorDarks
Copy link

I think we have to create new issue about it, since particularly that issue related to map-get function improvement.

@tjbenton
Copy link

@chriseppstein Reply to (comment) about map merge. I can see this working in a couple of different ways. Going under the assumption map-get is implemented like map-get($map, foo, bar, baz).

  1. a. Update map-merge to work like this awesome extend function written by @hugogiraudel. This way map-merge is strickly for merging multiple maps, and it adds a recursive functionality that wouldn't break the current implementation.
    b. Add a new map-set function that would work exactly like map-get except the last argument would be the value that's set.

    $map: (
     foo: (
      bar: (
       baz: (
        qux: "qux"
       )
      )
     )
    );
    
    // setting a value would replace the current value of `qux` to be "waldo"
    $map: map-set($map, foo, bar, baz, qux, "waldo");
    
    // Combining `map-set` with `map-merge`
    $map: map-set($map, foo, bar, baz, map-merge(map-get($map, foo, bar, baz), (
           quux: true,
           garply: false
          )));

    The down side would be having to combine it with map-get for combining deep maps. But the upside is that you leave the functionality of map-merge to just merge maps which is what I would think it would do based off the name. For more examples on how map-set would be useful see below.

  2. a. Update map-merge to work just like map-get except the last argument would be the map to merge

    $map: map-merge($map, foo, bar, baz, (fox: true));

    It would return the map of the first argument's map in the arguments list, not the 2nd to last(aka: baz) arguments map.
    b. Add a new map-extend extend function. It solves a lot of merging issues you may run into when merging multiple maps, and it's recursive.

  3. A quazi hybrid of map-get, and @hugogiraudel extend function

    // @arg {list} $get - A list where the last item is the map to merge
    // @arg {ArgList} $maps... - A list of maps you want to merge
    // @arg {bool} $recursive [false] - recursive mode
    // map-merge($get, $maps.../*, $recursive */);
    $map-4: map-merge($map foo bar baz, $map-1, $map-2, $map-3, true);

All 3 map-merge options are backwards compatable to sass 3.3+. So they're all viable options. Personally I like #1 the most because it would match map-get closer for consistency, and because I'm not a huge fan of space delimited lists used in #2. But I can see how #2 is appealing because no new functions have to be added to deal with map merging and it still adds all the functionality of the extend.

No matter which is direction is chosen for dealing with merging maps, I would still like to see a map-set function added that's specifically used for setting values in a map. I think it would be useful because sometimes you need to update a value of a map and at the same time remove some keys from a map, and sometimes you just want to set a single key in a map; and, in my opinion, writing map-merge($map, (foo: "bar")) to set a single value in a map is weird.

If map-set was implemented then there wouldn't be a need to update map-remove which would be good because since map-remove already uses a an args list and dot notation isn't gonna happen, the first argument would have to be a space delimited list to get to deep values, and then it would have to return the map of first item in the list. Here are some examples more examples of map-set combined with map-remove, and how you would do it without map-set.

$map: (
 foo: (
  bar: (
   baz: "baz",
   qux: "qux",
   quux: "quux",
   corge: "corge",
   grault: "grault"
  )
 ),
);
  • Note: map-remove would still return a new map of the map that it was passed.
  • Note: these examples are using map-merge suggestion #1 from above

Examples: With map-set

Remove values of a nested map, and not update values.
// 80 characters, 6 parentheses
$map: map-set($map, foo, bar, map-remove(map-get($map, foo, bar), qux, quux));
Removing qux, and quux, and setting baz to "waldo"
// 101 characters, 8 parentheses
$map: map-set($map, foo, bar, map-set(map-remove(map-get($map, foo, bar), qux, quux), baz, "waldo"));
Setting a value of a nested map
// 44 characters, 2 parentheses
$map: map-set($map, foo, bar, baz, "waldo");

Examples: The same examples without map-set

Remove values of a nested map, and not update values.
// 84 characters, 8 parentheses
$map: map-merge($map, foo, (bar: map-remove(map-get($map, foo, bar), qux, quux)));
Removing qux, and quux, and setting baz to "waldo"
// 109 characters, 12 parentheses
$map: map-merge($map, foo, (bar: map-merge(map-remove(map-get($map, foo, bar), qux, quux), (baz: "waldo"))));
Setting a value of a nested map
// 48 characters, 4 parentheses
$map: map-merge($map, foo, bar, (baz: "waldo"));

In all cases map-set uses less characters and parentheses than map-merge, and less which increases readability. As mentioned before this would leave the functionality of map-merge to be just for merging maps which in my opinion would very good thing. Hope this leads to a solution for this issue.

@nex3
Copy link
Contributor

nex3 commented Jul 17, 2015

This thread is getting extremely off-topic, so I'm locking it. In summary, we will add support for multiple keys to map-get and map-merge with the following semantics:

$map: (a: (b: (c: d)))
map-get($map, a, b, c) // => d
map-merge($map, a, b, c, x) // => (a: (b: (c: x)))

@sass sass locked and limited conversation to collaborators Jul 17, 2015
@nex3 nex3 added the help wanted Extra attention is needed label Aug 21, 2015
@nex3 nex3 changed the title Support map-get-z Support nested maps with "map-get" Aug 29, 2015
@lunelson
Copy link

I agree that separate map-set and map-merge functions, with a restriction on arguments of the latter will make them more semantically clear 👍

HamptonMakes added a commit to HamptonMakes/sass that referenced this issue Feb 1, 2016
Now, map-merge, map-get, and map-has-key all accept various forms of nested Map logic as per @nex3's specifications in sass#1739, with the addition of a nested compatible map-has-key.
@ArmorDarks
Copy link

I wonder, is that feature has been put on hold?

@lunelson
Copy link

lunelson commented Jan 8, 2019

I've created a small library of functions which transparently provide the API as proposed here; they provide nested map-set as well as nested map-get and map-merge operations (native functions folded in and replaced), so you can just drop them in and use this API until it's natively supported. They are tested for compatibility with both libsass and dart-sass:

https://github.com/lunelson/sass-maps-next

@mesqueeb
Copy link

Is anyone still working on this planned feature? Or has the plan been deprecated.

@mirisuzanne
Copy link
Contributor

There's a formal proposal in development. See the links directly above.

@nex3
Copy link
Contributor

nex3 commented Sep 22, 2020

I forgot to mention: since the proposal was accepted and the blog post soliciting comments published on 16 September, the countdown has begun for public comment. I'll give it two weeks with the option of going longer if there's substantial discussion. If there isn't, on 30 September it will be locked in and we'll ship the feature in Dart Sass.

@nex3 nex3 added proposal accepted A full proposal for this feature has been marked accepted specs written Specs have been written for the feature and at least one implementation passes them and removed help wanted Extra attention is needed proposal accepted A full proposal for this feature has been marked accepted labels Oct 6, 2020
@nex3
Copy link
Contributor

nex3 commented Oct 26, 2020

Closing this out because LibSass is now deprecated and we aren't expecting to add any additional features to it.

@nex3 nex3 closed this as completed Oct 26, 2020
@mesqueeb
Copy link

Thanks! will there be a guide how to migrate from node-sass to dart-sass for front-end non-dart JavaScript applications & websites?

@nex3
Copy link
Contributor

nex3 commented Oct 27, 2020

Dart Sass supports the same JavaScript API as Node Sass, so just replace node-sass with sass in your package.json.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request planned We would like to add this feature at some point specs written Specs have been written for the feature and at least one implementation passes them
Projects
None yet
Development

No branches or pull requests