Skip to content

Commit cff3cc7

Browse files
committed
Address feedback, and add deep-merge function
1 parent 43d8407 commit cff3cc7

File tree

1 file changed

+155
-37
lines changed

1 file changed

+155
-37
lines changed

proposal/nested-map-functions.md

Lines changed: 155 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,51 @@ setting, and getting items from nested maps.
99

1010
* [Background](#background)
1111
* [Summary](#summary)
12-
* [Syntax](#syntax)
12+
* [Functions](#functions)
1313
* [`get()`](#get)
1414
* [`has-key()`](#has-key)
1515
* [`set()`](#set)
1616
* [`merge()`](#merge)
17+
* [`deep-merge()`](#deep-merge)
1718

1819
## Background
1920

2021
> This section is non-normative.
2122
22-
The current map inspection and manipulation functions don't provide any built-in
23-
support for managing nested maps. Projects often build thir own tooling, but
24-
the results are inconsistent, and often slow.
23+
Variables have always been a key feature of the Sass language. But these days,
24+
design systems and component libraries form the basis of most CSS projects --
25+
with well organized _design tokens_ as the foundation. While Individual token
26+
variables can be quite useful, the ability to group tokens into strucutred and
27+
meaningful relationships is essential for creating resilient systems.
28+
29+
There are many ways to group tokens. The popular Style Dictionary recommends
30+
a deep nesting of _category_, _type_, _item_, _sub-item_, and _state_. Other
31+
taxonomies also include concepts like _theme_, or even _operating system_. Most
32+
of the existing tools rely on YAML or JSON objects to achieve that nested
33+
structure, at the expense of other important information. YAML and JSON are not
34+
design languages, and do not understand fundamental CSS concepts like color or
35+
length.
36+
37+
With Sass, we don't have to make that tradeoff. We already support nestable map
38+
structures, and the ability to interact with them programmatically -- adding or
39+
removing properties, accessing values, and looping over entire structures. But
40+
current built-in functions don't provide much support for managing nested maps.
41+
Projects often build thir own tooling.
42+
43+
The results are inconsistent across projects, difficult to re-use, and often
44+
slow to compile. Implementing core support for nested maps could change all that.
2545

2646
## Summary
2747

2848
> This section is non-normative.
2949
30-
This proposal updates the `sass:map` module to better support inspection and
31-
manipulation of nested maps.
50+
This proposal updates existing map functions with better support for inspection
51+
and manipulation of nested maps, as well as adding new functions to the
52+
`sass:map` module. For existing legacy functions (`get()`, `has-key()`,
53+
`merge()`) the new behavior will be accessible through both the `sass:map`
54+
module, and global legacy names (`map-get()`, `map-has-key()`, `map-merge()`).
55+
New functions (`set()`, `deep-merge()`) will only be available inside the
56+
`sass:map` module.
3257

3358
The `has-key()` and `get()` functions both accept multiple `$keys...`:
3459

@@ -89,17 +114,65 @@ $nav: map.set($nav, 'color', 'hover', 'search', green);
89114
$nav: map.set($nav, 'color', 'hover', 'logo', orange);
90115
```
91116

92-
## Syntax
117+
And finally, a new `deep-merge()` function in the `sass:map` module allows
118+
merging two or more nested maps. This works much like the existing `merge()`
119+
function, but when both maps have a nested-map at the same key, those nested
120+
maps are also merged:
121+
122+
```scss
123+
@use 'sass:map';
124+
125+
$nav: (
126+
'bg': 'gray',
127+
'color': (
128+
'hover': (
129+
'search': yellow,
130+
'home': red,
131+
'filter': blue,
132+
),
133+
),
134+
);
135+
136+
$update: (
137+
'bg': white,
138+
'color': (
139+
'hover': (
140+
'search': green,
141+
'logo': orange,
142+
)
143+
)
144+
);
145+
146+
$nav: map.deep-merge($nav, $update);
147+
148+
// $nav: (
149+
// 'bg': white,
150+
// 'color': (
151+
// 'hover': (
152+
// 'search': green,
153+
// 'home': red,
154+
// 'filter': blue,
155+
// 'logo': orange,
156+
// ),
157+
// ),
158+
// );
159+
```
160+
161+
## Functions
93162

94163
### `get()`
95164

96165
```
97166
get($map, $keys...)
98167
```
99168

169+
* If `$map` is not a map, throw an error.
170+
171+
* If `$keys` is empty, throw an error.
172+
100173
* Let `key` be the first (or only) element in `$keys`
101174

102-
* If `$map` does not have a key with the same name as `key`, throw an error.
175+
* If `$map` does not have a key equal to `key`, return `null`.
103176

104177
* Let `value` be the value assigned to `key` in `$map`
105178

@@ -117,6 +190,10 @@ get($map, $keys...)
117190
has-key($map, $keys...)
118191
```
119192

193+
* If `$map` is not a map, throw an error.
194+
195+
* If `$keys` is empty, throw an error.
196+
120197
* Let `key` be the first (or only) element in `$keys`
121198

122199
* If `$map` does not have a key with the same name as `key`, return boolean
@@ -135,22 +212,22 @@ has-key($map, $keys...)
135212
### `set()`
136213

137214
```
138-
set($map1, $keys..., $value)
215+
set($map, $args...)
139216
```
140217

141-
* If `$map1` is not a map, throw an error.
218+
* If `$map` is not a map, throw an error.
142219

143-
* If fewer than three arguments are provided, throw an error.
220+
* Let `map` be an empty map.
144221

145-
* Let `map` be an empty map .
222+
* Let `set-key` be the first item in arglist `$args`.
146223

147-
* If there is more than one argument in arglist `$keys`:
224+
* Let `remaining` be a slice of all the other items in arglist `$args`.
148225

149-
* Let `set-key` be the first element in `$keys`.
226+
* If there are no items in `remaining`, throw an error.
150227

151-
* Let `keys` be a slice of all `$keys` elements except the first.
228+
* If there is more than one argument in `remaining`:
152229

153-
* If `$map1` has a key `current-key` with the same name as `set-key`:
230+
* If `$map` has a key `current-key` that is equal to `set-key`:
154231

155232
* Let `current` be the value of `current-key`.
156233

@@ -159,46 +236,87 @@ set($map1, $keys..., $value)
159236
* Let `current` be an empty map.
160237

161238
* Let `set-value` be the result of calling `set()` with `current` and expanded
162-
`keys` as arguments.
239+
`remaining` as arguments.
163240

164-
> This will error if `current` is not a map, but we stil have nested `keys`
241+
> This will error if `current` is not a map, but we stil have `remaining`
165242
166243
* Otherwise:
167244

168-
* Let `set-key` be the only element in `$keys`
245+
* Let `set-value` be the only item in `remaining`
169246

170-
* Let `set-value` be the value of `$value`
247+
* Return a copy of `map` with `set-key` set to `set-value`, overriding an
248+
existing value for `set-key` if one exists.
171249

172-
* Add a key with name `set-key` and value `set-value` to `map`
250+
### `merge()`
173251

174-
* For each `key`, `value` pair in `$map1`
252+
This proposal add a new overload to the existing `merge()` function:
175253

176-
* If a key with name `key` already exists in `map`, do nothing.
254+
```
255+
merge($map1, $args...)
256+
```
177257

178-
* Otherwise, add a key to `map` with name `key` and value `value`.
258+
* If `$args` is empty, throw an error
179259

180-
* Return the value `map`
260+
* Let `map2` be the last item in arglist `$args`
181261

182-
### `merge()`
262+
* If either `$map1` or `map2` is not a map, throw an error.
263+
264+
* If arglist `$args` has more than one item:
265+
266+
* Let `get-keys` be a slice of all except the last item in `args`.
267+
268+
* Let `sub` be the result of calling `get()` with `sub` and expanded `get-keys`
269+
as arguments.
270+
271+
* Let `sub-merged` be the result of calling `merge()` with `sub` and `map2`
272+
as argumets.
273+
274+
* Let `set-args` be the result of appending `sub-merged` to the list `keys`
275+
276+
* Return the result of calling `set()` with `$map1` and expanded `set-args`
277+
as arguments.
278+
279+
* Otherwise:
280+
281+
* Let `map` be a copy of `$map1`.
282+
283+
* For each `key`, `value` in `map2`:
284+
285+
* Replace `map` with the result of calling `set()` with `map`, `key`, and
286+
`value` as arguments.
287+
288+
* Return `map`.
289+
290+
### `deep-merge()`
183291

184292
```
185-
merge($map1, $keys..., $map2)
293+
deep-merge($maps...)
186294
```
187295

188-
* If only one argument is provided, throw an error.
296+
* If the length of `$maps` is less than two, throw an error.
189297

190-
* If either the first (`$map1`) or last (`$map2`) argument is not a map, throw
191-
an error.
298+
* If any of the items in `$maps` are not maps, throw an error.
192299

193-
* Let `map` be an empty map.
300+
* Let `merged` be an empty map.
301+
302+
* For each `map` in `maps`:
303+
304+
* for each `key`, `value` in `map`:
305+
306+
* If `value` is a map:
307+
308+
* Let `current` be the result of calling `get()` with `merged` and `key` as
309+
arguments.
194310

195-
* For each `key`, `new` pair in `$map2`:
311+
* If `current` is a map:
196312

197-
* If `$map1` has a key with the same name as `key`
313+
* Let `deep` be the result of calling `deep-merge()` with `current` and
314+
`value` as arguments.
198315

199-
* Let `keys` be the result of appending `key` to the argument list `$keys`.
316+
* Replace `map` with the result of calling `set()` with `map`, `key`, and
317+
`deep` as arguments.
200318

201-
* Update `map` to be the result of calling `set()` with `map`,
202-
expanded `keys`, and `value` as arguments.
319+
* Replace `merged` with the result of calling `merge` with `merged` and `map`
320+
as arguments.
203321

204-
* Return the value of `map`
322+
* Return `merged`.

0 commit comments

Comments
 (0)