Skip to content

Commit ae83edd

Browse files
committed
Support Map and Set in #each block
Also support Map-keys in lookup-expressions. See #1418 #1679
1 parent 8ce2be4 commit ae83edd

File tree

7 files changed

+100
-12
lines changed

7 files changed

+100
-12
lines changed

lib/handlebars/helpers/each.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Exception } from '@handlebars/parser';
2-
import { createFrame, isArray, isFunction } from '../utils';
2+
import { createFrame, isArray, isFunction, isMap, isSet } from '../utils';
33

44
export default function (instance) {
55
instance.registerHelper('each', function (context, options) {
@@ -21,7 +21,7 @@ export default function (instance) {
2121
data = createFrame(options.data);
2222
}
2323

24-
function execIteration(field, index, last) {
24+
function execIteration(field, value, index, last) {
2525
if (data) {
2626
data.key = field;
2727
data.index = index;
@@ -31,7 +31,7 @@ export default function (instance) {
3131

3232
ret =
3333
ret +
34-
fn(context[field], {
34+
fn(value, {
3535
data: data,
3636
blockParams: [context[field], field],
3737
});
@@ -41,9 +41,19 @@ export default function (instance) {
4141
if (isArray(context)) {
4242
for (let j = context.length; i < j; i++) {
4343
if (i in context) {
44-
execIteration(i, i, i === context.length - 1);
44+
execIteration(i, context[i], i, i === context.length - 1);
4545
}
4646
}
47+
} else if (isMap(context)) {
48+
const j = context.size;
49+
for (const [key, value] of context) {
50+
execIteration(key, value, i++, i === j);
51+
}
52+
} else if (isSet(context)) {
53+
const j = context.size;
54+
for (const value of context) {
55+
execIteration(i, value, i++, i === j);
56+
}
4757
} else if (typeof Symbol === 'function' && context[Symbol.iterator]) {
4858
const newContext = [];
4959
const iterator = context[Symbol.iterator]();
@@ -52,7 +62,7 @@ export default function (instance) {
5262
}
5363
context = newContext;
5464
for (let j = context.length; i < j; i++) {
55-
execIteration(i, i, i === context.length - 1);
65+
execIteration(i, context[i], i, i === context.length - 1);
5666
}
5767
} else {
5868
let priorKey;
@@ -62,13 +72,13 @@ export default function (instance) {
6272
// the last iteration without have to scan the object twice and create
6373
// an intermediate keys array.
6474
if (priorKey !== undefined) {
65-
execIteration(priorKey, i - 1);
75+
execIteration(priorKey, context[priorKey], i - 1);
6676
}
6777
priorKey = key;
6878
i++;
6979
});
7080
if (priorKey !== undefined) {
71-
execIteration(priorKey, i - 1, true);
81+
execIteration(priorKey, context[priorKey], i - 1, true);
7282
}
7383
}
7484
}

lib/handlebars/runtime.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ export function template(templateSpec, env) {
124124
return container.lookupProperty(obj, name);
125125
},
126126
lookupProperty: function (parent, propertyName) {
127+
if (Utils.isMap(parent)) {
128+
return parent.get(propertyName);
129+
}
130+
127131
let result = parent[propertyName];
128132
if (result == null) {
129133
return result;

lib/handlebars/utils.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,18 @@ export function isFunction(value) {
3535
return typeof value === 'function';
3636
}
3737

38-
/* istanbul ignore next */
39-
export const isArray =
40-
Array.isArray ||
41-
function (value) {
38+
function testTag(name) {
39+
const tag = '[object ' + name + ']';
40+
return function (value) {
4241
return value && typeof value === 'object'
43-
? toString.call(value) === '[object Array]'
42+
? toString.call(value) === tag
4443
: false;
4544
};
45+
}
46+
47+
export const isArray = Array.isArray;
48+
export const isMap = testTag('Map');
49+
export const isSet = testTag('Set');
4650

4751
// Older IE versions do not directly support indexOf so we must implement our own, sadly.
4852
export function indexOf(array, value) {

spec/basic.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,13 @@ describe('basic context', function () {
387387
.toCompileTo('Goodbye beautiful world!');
388388
});
389389

390+
it('nested paths with Map', function () {
391+
expectTemplate('Goodbye {{alan/expression}} world!')
392+
.withInput({ alan: new Map([['expression', 'beautiful']]) })
393+
.withMessage('Nested paths access nested objects')
394+
.toCompileTo('Goodbye beautiful world!');
395+
});
396+
390397
it('nested paths with empty string value', function () {
391398
expectTemplate('Goodbye {{alan/expression}} world!')
392399
.withInput({ alan: { expression: '' } })

spec/builtins.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,50 @@ describe('builtin helpers', function () {
508508
);
509509
});
510510

511+
it('each on Map', function () {
512+
var map = new Map([
513+
[1, 'one'],
514+
[2, 'two'],
515+
[3, 'three'],
516+
]);
517+
518+
expectTemplate('{{#each map}}{{@key}}(i{{@index}}) {{.}} {{/each}}')
519+
.withInput({ map: map })
520+
.toCompileTo('1(i0) one 2(i1) two 3(i2) three ');
521+
522+
expectTemplate('{{#each map}}{{#if @first}}{{.}}{{/if}}{{/each}}')
523+
.withInput({ map: map })
524+
.toCompileTo('one');
525+
526+
expectTemplate('{{#each map}}{{#if @last}}{{.}}{{/if}}{{/each}}')
527+
.withInput({ map: map })
528+
.toCompileTo('three');
529+
530+
expectTemplate('{{#each map}}{{.}}{{/each}}not-in-each')
531+
.withInput({ map: new Map() })
532+
.toCompileTo('not-in-each');
533+
});
534+
535+
it('each on Set', function () {
536+
var set = new Set([1, 2, 3]);
537+
538+
expectTemplate('{{#each set}}{{@key}}(i{{@index}}) {{.}} {{/each}}')
539+
.withInput({ set: set })
540+
.toCompileTo('0(i0) 1 1(i1) 2 2(i2) 3 ');
541+
542+
expectTemplate('{{#each set}}{{#if @first}}{{.}}{{/if}}{{/each}}')
543+
.withInput({ set: set })
544+
.toCompileTo('1');
545+
546+
expectTemplate('{{#each set}}{{#if @last}}{{.}}{{/if}}{{/each}}')
547+
.withInput({ set: set })
548+
.toCompileTo('3');
549+
550+
expectTemplate('{{#each set}}{{.}}{{/each}}not-in-each')
551+
.withInput({ set: new Set() })
552+
.toCompileTo('not-in-each');
553+
});
554+
511555
if (global.Symbol && global.Symbol.iterator) {
512556
it('each on iterable', function () {
513557
function Iterator(arr) {

spec/utils.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,21 @@ describe('utils', function () {
8686
equals(b.b, 2);
8787
});
8888
});
89+
90+
describe('#isType', function () {
91+
it('should check if variable is type Array', function () {
92+
expect(Handlebars.Utils.isArray('string')).to.equal(false);
93+
expect(Handlebars.Utils.isArray([])).to.equal(true);
94+
});
95+
96+
it('should check if variable is type Map', function () {
97+
expect(Handlebars.Utils.isMap('string')).to.equal(false);
98+
expect(Handlebars.Utils.isMap(new Map())).to.equal(true);
99+
});
100+
101+
it('should check if variable is type Set', function () {
102+
expect(Handlebars.Utils.isSet('string')).to.equal(false);
103+
expect(Handlebars.Utils.isSet(new Set())).to.equal(true);
104+
});
105+
});
89106
});

types/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ declare namespace Handlebars {
123123
export function toString(obj: any): string;
124124
export function isArray(obj: any): boolean;
125125
export function isFunction(obj: any): boolean;
126+
export function isMap(obj: any): boolean;
127+
export function isSet(obj: any): boolean;
126128
}
127129

128130
export namespace AST {

0 commit comments

Comments
 (0)