Support Map and Set in #each block
Also support Map-keys in lookup-expressions. See #1418 #1679
This commit is contained in:
committed by
Jay Linski
parent
8ce2be4496
commit
ae83edd5c4
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: '' } })
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+2
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user