Skip to content

Commit

Permalink
Support Map and Set in #each block
Browse files Browse the repository at this point in the history
Also support Map-keys in lookup-expressions.

See #1418 #1679
  • Loading branch information
jaylinski committed Sep 6, 2023
1 parent 8ce2be4 commit ae83edd
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 12 deletions.
24 changes: 17 additions & 7 deletions 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) {
Expand All @@ -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;
Expand All @@ -31,7 +31,7 @@ export default function (instance) {

ret =
ret +
fn(context[field], {
fn(value, {
data: data,
blockParams: [context[field], field],
});
Expand All @@ -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]();
Expand All @@ -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;
Expand All @@ -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);
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions lib/handlebars/runtime.js
Expand Up @@ -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;
Expand Down
14 changes: 9 additions & 5 deletions lib/handlebars/utils.js
Expand Up @@ -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) {
Expand Down
7 changes: 7 additions & 0 deletions spec/basic.js
Expand Up @@ -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: '' } })
Expand Down
44 changes: 44 additions & 0 deletions spec/builtins.js
Expand Up @@ -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) {
Expand Down
17 changes: 17 additions & 0 deletions spec/utils.js
Expand Up @@ -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);
});
});
});
2 changes: 2 additions & 0 deletions types/index.d.ts
Expand Up @@ -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 {
Expand Down

0 comments on commit ae83edd

Please sign in to comment.