From 836e12b1c5936e4f6a7530deab408e2673c491af Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Wed, 12 Jul 2023 09:37:11 +0200 Subject: [PATCH] [IMP] runtime: add support for Map and other iterables in t-foreach closes: #1352 --- doc/reference/templates.md | 15 +-- src/runtime/template_helpers.ts | 22 ++-- .../__snapshots__/t_foreach.test.ts.snap | 112 ++++++++++++++++++ tests/compiler/t_foreach.test.ts | 62 +++++++++- 4 files changed, 196 insertions(+), 15 deletions(-) diff --git a/doc/reference/templates.md b/doc/reference/templates.md index 2e27812f..705728a8 100644 --- a/doc/reference/templates.md +++ b/doc/reference/templates.md @@ -376,15 +376,16 @@ An important difference should be made with the usual `QWeb` behaviour: Owl requires the presence of a `t-key` directive, to be able to properly reconcile renderings. -`t-foreach` can iterate on an array (the current item will be the current value) -or an object (the current item will be the current key). +`t-foreach` can iterate on any iterable, and also has special support for objects +and maps, it will expose the key of the current iteration as the contents of the +`t-as`, and the corresponding value with the same name and the suffix `_value`. -In addition to the name passed via t-as, `t-foreach` provides a few other -variables for various data points (note: `$as` will be replaced with the name -passed to `t-as`): +In addition to the name passed via t-as, `t-foreach` provides a few other useful +variables (note: `$as` will be replaced with the name passed to `t-as`): -- `$as_value`: the current iteration value, identical to `$as` for lists and - integers, but for objects, it provides the value (where `$as` provides the key) +- `$as_value`: the current iteration value, identical to `$as` for arrays and + other iterables, but for objects and maps, it provides the value (where `$as` + provides the key) - `$as_index`: the current iteration index (the first item of the iteration has index 0) - `$as_first`: whether the current item is the first of the iteration (equivalent to `$as_index == 0`) diff --git a/src/runtime/template_helpers.ts b/src/runtime/template_helpers.ts index 239e1029..7c553b07 100644 --- a/src/runtime/template_helpers.ts +++ b/src/runtime/template_helpers.ts @@ -60,18 +60,26 @@ function withKey(elem: any, k: string) { return elem; } -function prepareList(collection: any): [any[], any[], number, any[]] { - let keys: any[]; - let values: any[]; +function prepareList(collection: unknown): [unknown[], unknown[], number, undefined[]] { + let keys: unknown[]; + let values: unknown[]; if (Array.isArray(collection)) { keys = collection; values = collection; - } else if (collection) { - values = Object.keys(collection); - keys = Object.values(collection); + } else if (collection instanceof Map) { + keys = [...collection.keys()]; + values = [...collection.values()]; + } else if (collection && typeof collection === "object") { + if (Symbol.iterator in collection) { + keys = [...(>collection)]; + values = keys; + } else { + values = Object.keys(collection); + keys = Object.values(collection); + } } else { - throw new OwlError("Invalid loop expression"); + throw new OwlError(`Invalid loop expression: "${collection}" is not iterable`); } const n = values.length; return [keys, values, n, new Array(n)]; diff --git a/tests/compiler/__snapshots__/t_foreach.test.ts.snap b/tests/compiler/__snapshots__/t_foreach.test.ts.snap index 3ec3c63c..1dcee101 100644 --- a/tests/compiler/__snapshots__/t_foreach.test.ts.snap +++ b/tests/compiler/__snapshots__/t_foreach.test.ts.snap @@ -77,6 +77,62 @@ exports[`t-foreach iterate on items 1`] = ` }" `; +exports[`t-foreach iterate, Map param 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { prepareList, withKey } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['value']);; + for (let i1 = 0; i1 < l_block1; i1++) { + ctx[\`item\`] = v_block1[i1]; + ctx[\`item_index\`] = i1; + ctx[\`item_value\`] = k_block1[i1]; + const key1 = ctx['item_index']; + const b3 = text(\` [\`); + const b4 = text(ctx['item_index']); + const b5 = text(\`: \`); + const b6 = text(ctx['item']); + const b7 = text(\` \`); + const b8 = text(ctx['item_value']); + const b9 = text(\`] \`); + c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1); + } + return list(c_block1); + } +}" +`; + +exports[`t-foreach iterate, Set param 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { prepareList, withKey } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['value']);; + for (let i1 = 0; i1 < l_block1; i1++) { + ctx[\`item\`] = v_block1[i1]; + ctx[\`item_index\`] = i1; + ctx[\`item_value\`] = k_block1[i1]; + const key1 = ctx['item_index']; + const b3 = text(\` [\`); + const b4 = text(ctx['item_index']); + const b5 = text(\`: \`); + const b6 = text(ctx['item']); + const b7 = text(\` \`); + const b8 = text(ctx['item_value']); + const b9 = text(\`] \`); + c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1); + } + return list(c_block1); + } +}" +`; + exports[`t-foreach iterate, dict param 1`] = ` "function anonymous(app, bdom, helpers ) { @@ -108,6 +164,62 @@ exports[`t-foreach iterate, dict param 1`] = ` }" `; +exports[`t-foreach iterate, generator param 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { prepareList, withKey } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['gen']());; + for (let i1 = 0; i1 < l_block1; i1++) { + ctx[\`item\`] = v_block1[i1]; + ctx[\`item_index\`] = i1; + ctx[\`item_value\`] = k_block1[i1]; + const key1 = ctx['item_index']; + const b3 = text(\` [\`); + const b4 = text(ctx['item_index']); + const b5 = text(\`: \`); + const b6 = text(ctx['item']); + const b7 = text(\` \`); + const b8 = text(ctx['item_value']); + const b9 = text(\`] \`); + c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1); + } + return list(c_block1); + } +}" +`; + +exports[`t-foreach iterate, iterable param 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { prepareList, withKey } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['map'].values());; + for (let i1 = 0; i1 < l_block1; i1++) { + ctx[\`item\`] = v_block1[i1]; + ctx[\`item_index\`] = i1; + ctx[\`item_value\`] = k_block1[i1]; + const key1 = ctx['item_index']; + const b3 = text(\` [\`); + const b4 = text(ctx['item_index']); + const b5 = text(\`: \`); + const b6 = text(ctx['item']); + const b7 = text(\` \`); + const b8 = text(ctx['item_value']); + const b9 = text(\`] \`); + c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1); + } + return list(c_block1); + } +}" +`; + exports[`t-foreach iterate, position 1`] = ` "function anonymous(app, bdom, helpers ) { diff --git a/tests/compiler/t_foreach.test.ts b/tests/compiler/t_foreach.test.ts index 9a5fd992..7605b21b 100644 --- a/tests/compiler/t_foreach.test.ts +++ b/tests/compiler/t_foreach.test.ts @@ -105,6 +105,64 @@ describe("t-foreach", () => { expect(renderToString(template, context)).toBe(expected); }); + test("iterate, Map param", () => { + const template = ` + + [: ] + `; + const expected = ` [0: 1 a] [1: 2 b] [2: 3 c] `; + const context = { + value: new Map([ + ["a", 1], + ["b", 2], + ["c", 3], + ]), + }; + expect(renderToString(template, context)).toBe(expected); + }); + + test("iterate, Set param", () => { + const template = ` + + [: ] + `; + const expected = ` [0: 1 1] [1: 2 2] [2: 3 3] `; + const context = { value: new Set([1, 2, 3]) }; + expect(renderToString(template, context)).toBe(expected); + }); + + test("iterate, iterable param", () => { + const template = ` + + [: ] + `; + const expected = ` [0: 1 1] [1: 2 2] [2: 3 3] `; + const context = { + map: new Map([ + ["a", 1], + ["b", 2], + ["c", 3], + ]), + }; + expect(renderToString(template, context)).toBe(expected); + }); + + test("iterate, generator param", () => { + const template = ` + + [: ] + `; + const expected = ` [0: 1 1] [1: 2 2] [2: 3 3] `; + const context = { + *gen() { + yield 1; + yield 2; + yield 3; + }, + }; + expect(renderToString(template, context)).toBe(expected); + }); + test("does not pollute the rendering context", () => { const template = `
@@ -193,7 +251,9 @@ describe("t-foreach", () => { test("throws error if invalid loop expression", () => { const test = `
`; - expect(() => renderToString(test)).toThrow("Invalid loop expression"); + expect(() => renderToString(test)).toThrow( + 'Invalid loop expression: "undefined" is not iterable' + ); }); test("t-foreach with t-if inside", () => {