Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Map and Set in #each block #1996

Merged
merged 1 commit into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 17 additions & 7 deletions lib/handlebars/helpers/each.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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');
jaylinski marked this conversation as resolved.
Show resolved Hide resolved

// 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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