Support Map and Set in #each block

Also support Map-keys in lookup-expressions.

See #1418 #1679
This commit is contained in:
Jakob Linskeseder
2023-09-04 23:20:43 +02:00
committed by Jay Linski
parent 8ce2be4496
commit ae83edd5c4
7 changed files with 100 additions and 12 deletions
+17 -7
View File
@@ -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);
}
}
}
+4
View File
@@ -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;
+9 -5
View File
@@ -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) {
+7
View File
@@ -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: '' } })
+44
View File
@@ -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) {
+17
View File
@@ -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
View File
@@ -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 {