diff --git a/lib/handlebars/helpers/each.js b/lib/handlebars/helpers/each.js index 309c2786..c18f462a 100644 --- a/lib/handlebars/helpers/each.js +++ b/lib/handlebars/helpers/each.js @@ -1,5 +1,5 @@ import { Exception } from '@handlebars/parser'; -import { createFrame, isArray, isFunction } from '../utils'; +import { createFrame, isArray, isFunction, isMap, isSet } from '../utils'; export default function (instance) { instance.registerHelper('each', function (context, options) { @@ -21,7 +21,7 @@ export default function (instance) { data = createFrame(options.data); } - function execIteration(field, index, last) { + function execIteration(field, value, index, last) { if (data) { data.key = field; data.index = index; @@ -31,7 +31,7 @@ export default function (instance) { ret = ret + - fn(context[field], { + fn(value, { data: data, blockParams: [context[field], field], }); @@ -41,9 +41,19 @@ export default function (instance) { if (isArray(context)) { for (let j = context.length; i < j; i++) { if (i in context) { - execIteration(i, i, i === context.length - 1); + execIteration(i, context[i], i, i === context.length - 1); } } + } else if (isMap(context)) { + const j = context.size; + for (const [key, value] of context) { + execIteration(key, value, i++, i === j); + } + } else if (isSet(context)) { + const j = context.size; + for (const value of context) { + execIteration(i, value, i++, i === j); + } } else if (typeof Symbol === 'function' && context[Symbol.iterator]) { const newContext = []; const iterator = context[Symbol.iterator](); @@ -52,7 +62,7 @@ export default function (instance) { } context = newContext; for (let j = context.length; i < j; i++) { - execIteration(i, i, i === context.length - 1); + execIteration(i, context[i], i, i === context.length - 1); } } else { let priorKey; @@ -62,13 +72,13 @@ export default function (instance) { // the last iteration without have to scan the object twice and create // an intermediate keys array. if (priorKey !== undefined) { - execIteration(priorKey, i - 1); + execIteration(priorKey, context[priorKey], i - 1); } priorKey = key; i++; }); if (priorKey !== undefined) { - execIteration(priorKey, i - 1, true); + execIteration(priorKey, context[priorKey], i - 1, true); } } } diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 2a33f7bc..49842c34 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -124,6 +124,10 @@ export function template(templateSpec, env) { return container.lookupProperty(obj, name); }, lookupProperty: function (parent, propertyName) { + if (Utils.isMap(parent)) { + return parent.get(propertyName); + } + let result = parent[propertyName]; if (result == null) { return result; diff --git a/lib/handlebars/utils.js b/lib/handlebars/utils.js index c1ca0d44..dc9812fc 100644 --- a/lib/handlebars/utils.js +++ b/lib/handlebars/utils.js @@ -35,14 +35,18 @@ export function isFunction(value) { return typeof value === 'function'; } -/* istanbul ignore next */ -export const isArray = - Array.isArray || - function (value) { +function testTag(name) { + const tag = '[object ' + name + ']'; + return function (value) { return value && typeof value === 'object' - ? toString.call(value) === '[object Array]' + ? toString.call(value) === tag : false; }; +} + +export const isArray = Array.isArray; +export const isMap = testTag('Map'); +export const isSet = testTag('Set'); // Older IE versions do not directly support indexOf so we must implement our own, sadly. export function indexOf(array, value) { diff --git a/spec/basic.js b/spec/basic.js index b7f4f637..ef3885f8 100644 --- a/spec/basic.js +++ b/spec/basic.js @@ -387,6 +387,13 @@ describe('basic context', function () { .toCompileTo('Goodbye beautiful world!'); }); + it('nested paths with Map', function () { + expectTemplate('Goodbye {{alan/expression}} world!') + .withInput({ alan: new Map([['expression', 'beautiful']]) }) + .withMessage('Nested paths access nested objects') + .toCompileTo('Goodbye beautiful world!'); + }); + it('nested paths with empty string value', function () { expectTemplate('Goodbye {{alan/expression}} world!') .withInput({ alan: { expression: '' } }) diff --git a/spec/builtins.js b/spec/builtins.js index 4c1efe4e..e88327c4 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -508,6 +508,50 @@ describe('builtin helpers', function () { ); }); + it('each on Map', function () { + var map = new Map([ + [1, 'one'], + [2, 'two'], + [3, 'three'], + ]); + + expectTemplate('{{#each map}}{{@key}}(i{{@index}}) {{.}} {{/each}}') + .withInput({ map: map }) + .toCompileTo('1(i0) one 2(i1) two 3(i2) three '); + + expectTemplate('{{#each map}}{{#if @first}}{{.}}{{/if}}{{/each}}') + .withInput({ map: map }) + .toCompileTo('one'); + + expectTemplate('{{#each map}}{{#if @last}}{{.}}{{/if}}{{/each}}') + .withInput({ map: map }) + .toCompileTo('three'); + + expectTemplate('{{#each map}}{{.}}{{/each}}not-in-each') + .withInput({ map: new Map() }) + .toCompileTo('not-in-each'); + }); + + it('each on Set', function () { + var set = new Set([1, 2, 3]); + + expectTemplate('{{#each set}}{{@key}}(i{{@index}}) {{.}} {{/each}}') + .withInput({ set: set }) + .toCompileTo('0(i0) 1 1(i1) 2 2(i2) 3 '); + + expectTemplate('{{#each set}}{{#if @first}}{{.}}{{/if}}{{/each}}') + .withInput({ set: set }) + .toCompileTo('1'); + + expectTemplate('{{#each set}}{{#if @last}}{{.}}{{/if}}{{/each}}') + .withInput({ set: set }) + .toCompileTo('3'); + + expectTemplate('{{#each set}}{{.}}{{/each}}not-in-each') + .withInput({ set: new Set() }) + .toCompileTo('not-in-each'); + }); + if (global.Symbol && global.Symbol.iterator) { it('each on iterable', function () { function Iterator(arr) { diff --git a/spec/utils.js b/spec/utils.js index 0ef90e9b..0b081c20 100644 --- a/spec/utils.js +++ b/spec/utils.js @@ -86,4 +86,21 @@ describe('utils', function () { equals(b.b, 2); }); }); + + describe('#isType', function () { + it('should check if variable is type Array', function () { + expect(Handlebars.Utils.isArray('string')).to.equal(false); + expect(Handlebars.Utils.isArray([])).to.equal(true); + }); + + it('should check if variable is type Map', function () { + expect(Handlebars.Utils.isMap('string')).to.equal(false); + expect(Handlebars.Utils.isMap(new Map())).to.equal(true); + }); + + it('should check if variable is type Set', function () { + expect(Handlebars.Utils.isSet('string')).to.equal(false); + expect(Handlebars.Utils.isSet(new Set())).to.equal(true); + }); + }); }); diff --git a/types/index.d.ts b/types/index.d.ts index 4275c50f..f49c102e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -123,6 +123,8 @@ declare namespace Handlebars { export function toString(obj: any): string; export function isArray(obj: any): boolean; export function isFunction(obj: any): boolean; + export function isMap(obj: any): boolean; + export function isSet(obj: any): boolean; } export namespace AST {