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 { Exception } from '@handlebars/parser';
|
||||||
import { createFrame, isArray, isFunction } from '../utils';
|
import { createFrame, isArray, isFunction, isMap, isSet } from '../utils';
|
||||||
|
|
||||||
export default function (instance) {
|
export default function (instance) {
|
||||||
instance.registerHelper('each', function (context, options) {
|
instance.registerHelper('each', function (context, options) {
|
||||||
@@ -21,7 +21,7 @@ export default function (instance) {
|
|||||||
data = createFrame(options.data);
|
data = createFrame(options.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function execIteration(field, index, last) {
|
function execIteration(field, value, index, last) {
|
||||||
if (data) {
|
if (data) {
|
||||||
data.key = field;
|
data.key = field;
|
||||||
data.index = index;
|
data.index = index;
|
||||||
@@ -31,7 +31,7 @@ export default function (instance) {
|
|||||||
|
|
||||||
ret =
|
ret =
|
||||||
ret +
|
ret +
|
||||||
fn(context[field], {
|
fn(value, {
|
||||||
data: data,
|
data: data,
|
||||||
blockParams: [context[field], field],
|
blockParams: [context[field], field],
|
||||||
});
|
});
|
||||||
@@ -41,9 +41,19 @@ export default function (instance) {
|
|||||||
if (isArray(context)) {
|
if (isArray(context)) {
|
||||||
for (let j = context.length; i < j; i++) {
|
for (let j = context.length; i < j; i++) {
|
||||||
if (i in context) {
|
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]) {
|
} else if (typeof Symbol === 'function' && context[Symbol.iterator]) {
|
||||||
const newContext = [];
|
const newContext = [];
|
||||||
const iterator = context[Symbol.iterator]();
|
const iterator = context[Symbol.iterator]();
|
||||||
@@ -52,7 +62,7 @@ export default function (instance) {
|
|||||||
}
|
}
|
||||||
context = newContext;
|
context = newContext;
|
||||||
for (let j = context.length; i < j; i++) {
|
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 {
|
} else {
|
||||||
let priorKey;
|
let priorKey;
|
||||||
@@ -62,13 +72,13 @@ export default function (instance) {
|
|||||||
// the last iteration without have to scan the object twice and create
|
// the last iteration without have to scan the object twice and create
|
||||||
// an intermediate keys array.
|
// an intermediate keys array.
|
||||||
if (priorKey !== undefined) {
|
if (priorKey !== undefined) {
|
||||||
execIteration(priorKey, i - 1);
|
execIteration(priorKey, context[priorKey], i - 1);
|
||||||
}
|
}
|
||||||
priorKey = key;
|
priorKey = key;
|
||||||
i++;
|
i++;
|
||||||
});
|
});
|
||||||
if (priorKey !== undefined) {
|
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);
|
return container.lookupProperty(obj, name);
|
||||||
},
|
},
|
||||||
lookupProperty: function (parent, propertyName) {
|
lookupProperty: function (parent, propertyName) {
|
||||||
|
if (Utils.isMap(parent)) {
|
||||||
|
return parent.get(propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
let result = parent[propertyName];
|
let result = parent[propertyName];
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -35,14 +35,18 @@ export function isFunction(value) {
|
|||||||
return typeof value === 'function';
|
return typeof value === 'function';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* istanbul ignore next */
|
function testTag(name) {
|
||||||
export const isArray =
|
const tag = '[object ' + name + ']';
|
||||||
Array.isArray ||
|
return function (value) {
|
||||||
function (value) {
|
|
||||||
return value && typeof value === 'object'
|
return value && typeof value === 'object'
|
||||||
? toString.call(value) === '[object Array]'
|
? toString.call(value) === tag
|
||||||
: false;
|
: 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.
|
// Older IE versions do not directly support indexOf so we must implement our own, sadly.
|
||||||
export function indexOf(array, value) {
|
export function indexOf(array, value) {
|
||||||
|
|||||||
@@ -387,6 +387,13 @@ describe('basic context', function () {
|
|||||||
.toCompileTo('Goodbye beautiful world!');
|
.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 () {
|
it('nested paths with empty string value', function () {
|
||||||
expectTemplate('Goodbye {{alan/expression}} world!')
|
expectTemplate('Goodbye {{alan/expression}} world!')
|
||||||
.withInput({ alan: { expression: '' } })
|
.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) {
|
if (global.Symbol && global.Symbol.iterator) {
|
||||||
it('each on iterable', function () {
|
it('each on iterable', function () {
|
||||||
function Iterator(arr) {
|
function Iterator(arr) {
|
||||||
|
|||||||
@@ -86,4 +86,21 @@ describe('utils', function () {
|
|||||||
equals(b.b, 2);
|
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 toString(obj: any): string;
|
||||||
export function isArray(obj: any): boolean;
|
export function isArray(obj: any): boolean;
|
||||||
export function isFunction(obj: any): boolean;
|
export function isFunction(obj: any): boolean;
|
||||||
|
export function isMap(obj: any): boolean;
|
||||||
|
export function isSet(obj: any): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace AST {
|
export namespace AST {
|
||||||
|
|||||||
Reference in New Issue
Block a user