diff --git a/doc/reference/app.md b/doc/reference/app.md index 31a4d0591..2290e102f 100644 --- a/doc/reference/app.md +++ b/doc/reference/app.md @@ -117,7 +117,7 @@ const { loadFile, mount } = owl; Dev mode activates some additional checks and developer amenities: - [Props validation](./props.md#props-validation) is performed -- [t-foreach](./templates.md#loops) loops check for key unicity +- [t-for and t-foreach](./templates.md#loops) loops check for key unicity - Lifecycle hooks are wrapped to report their errors in a more developer-friendly way - onWillStart and onWillUpdateProps will emit a warning in the console when they take longer than 3 seconds in an effort to ease debugging the presence of deadlocks diff --git a/doc/reference/templates.md b/doc/reference/templates.md index 705728a8c..93bf4eca0 100644 --- a/doc/reference/templates.md +++ b/doc/reference/templates.md @@ -61,7 +61,7 @@ For reference, here is a list of all standard QWeb directives: | `t-out` | [Outputting value, possibly without escaping](#outputting-data) | | `t-set`, `t-value` | [Setting variables](#setting-variables) | | `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) | -| `t-foreach`, `t-as` | [Loops](#loops) | +| `t-for/t-of`, `t-foreach/t-as` | [Loops](#loops) | | `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) | | `t-call` | [Rendering sub templates](#sub-templates) | | `t-debug`, `t-log` | [Debugging](#debugging) | @@ -372,6 +372,24 @@ Like conditions, `t-foreach` applies to the element bearing the directive’s at is equivalent to the previous example. +Owl also has another pair of directives that can be used for looping that allow +destructuring the contents of the loop item: `t-for` and `t-of`, which behaves +much like `for..of` in javascript: + +```xml + +

:

+
+``` + +will be rendered as: + +```xml +

a: 1

+

b: 2

+

c: 3

+``` + 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. @@ -380,8 +398,9 @@ renderings. 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 useful -variables (note: `$as` will be replaced with the name passed to `t-as`): +In addition to the name passed via t-as, `t-foreach` (but not `t-for`) 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 arrays and other iterables, but for objects and maps, it provides the value (where `$as` @@ -393,10 +412,9 @@ variables (note: `$as` will be replaced with the name passed to `t-as`): (equivalent to `$as_index + 1 == $as_size`), requires the iteratee’s size be available -These extra variables provided and all new variables created into the `t-foreach` -are only available in the scope of the `t-foreach`. If the variable exists outside -the context of the `t-foreach`, the value is copied at the end of the foreach -into the global context. +These variables and all new variables created inside`t-foreach` and `t-for` are +only available inside of the loop. If a variable existed outside the context of +the loop, the assignment will affect the outer variable. ```xml @@ -408,7 +426,7 @@ into the global context.

- + ``` @@ -471,18 +489,11 @@ are all equivalent:
``` -If there is no `t-key` directive, Owl will use the index as a default key. - -Note: the `t-foreach` directive only accepts arrays (lists) or objects. It does -not work with other iterables, such as `Set`. However, it is only a matter of -using the `...` javascript operator. For example: - -```xml -... -``` - -The `...` operator will convert the `Set` (or any other iterables) into a list, -which will work with Owl QWeb. +The `t-key` directive is mandatory, and as mentioned should represent the object's +identity. You may be tempted to use the loop index as a key, but keep in mind that +this is only correct if items in the loop cannot be reordered. If this is not the +case, using the index as the key can lead to bugs that are difficult to find, so +use the index as the key only if you are sure items cannot be reordered. ### Sub Templates diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts index 3c0d6498c..e4c2d98c8 100644 --- a/src/compiler/code_generator.ts +++ b/src/compiler/code_generator.ts @@ -18,6 +18,7 @@ import { ASTTCallBlock, ASTTEsc, ASTText, + ASTTFor, ASTTForEach, ASTTif, ASTTKey, @@ -471,6 +472,8 @@ export class CodeGenerator { return this.compileTIf(ast, ctx); case ASTType.TForEach: return this.compileTForeach(ast, ctx); + case ASTType.TFor: + return this.compileTFor(ast, ctx); case ASTType.TKey: return this.compileTKey(ast, ctx); case ASTType.Multi: @@ -907,7 +910,7 @@ export class CodeGenerator { if (!ast.hasNoValue) { this.addLine(`ctx[\`${ast.elem}_value\`] = ${keys}[${loopVar}];`); } - this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar); + this.define(`key${this.target.loopLevel}`, compileExpr(ast.key)); if (this.dev) { // Throw error on duplicate keys in dev mode this.helpers.add("OwlError"); @@ -954,6 +957,46 @@ export class CodeGenerator { return block.varName; } + compileTFor(ast: ASTTFor, ctx: Context): string { + let { block } = ctx; + if (block) { + this.insertAnchor(block); + } + block = this.createBlock(block, "list", ctx); + this.target.loopLevel++; + this.addLine(`ctx = Object.create(ctx);`); + // Throw errors on duplicate keys in dev mode + if (this.dev) { + this.define(`keys${block.id}`, `new Set()`); + } + this.define(`c_block${block.id}`, "[]"); + const index = `i${this.target.loopLevel}`; + this.addLine(`let ${index} = ${0};`); + const binding = compileExpr(ast.binding); + this.addLine(`for (${binding} of ${compileExpr(ast.iterable)}) {`); + this.target.indentLevel++; + this.define(`key${this.target.loopLevel}`, compileExpr(ast.key)); + if (this.dev) { + // Throw error on duplicate keys in dev mode + this.helpers.add("OwlError"); + this.addLine( + `if (keys${block.id}.has(String(key${this.target.loopLevel}))) { throw new OwlError(\`Got duplicate key in t-for: \${key${this.target.loopLevel}}\`)}` + ); + this.addLine(`keys${block.id}.add(String(key${this.target.loopLevel}));`); + } + const subCtx = createContext(ctx, { block, index }); + this.compileAST(ast.body, subCtx); + this.addLine(`${index}++;`); + this.target.indentLevel--; + this.target.loopLevel--; + this.addLine(`}`); + if (!ctx.isLast) { + this.addLine(`ctx = ctx.__proto__;`); + } + this.insertBlock("l", block, ctx); + return block.varName; + } + compileTKey(ast: ASTTKey, ctx: Context): string | null { const tKeyExpr = generateId("tKey_"); this.define(tKeyExpr, compileExpr(ast.expr)); diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 5576f2948..c86bf6567 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -26,6 +26,7 @@ export const enum ASTType { TCallBlock, TTranslation, TPortal, + TFor, } export interface ASTText { @@ -104,7 +105,15 @@ export interface ASTTForEach { hasNoLast: boolean; hasNoIndex: boolean; hasNoValue: boolean; - key: string | null; + key: string; +} + +export interface ASTTFor { + type: ASTType.TFor; + iterable: string; + binding: string; + body: AST; + key: string; } export interface ASTTKey { @@ -183,6 +192,7 @@ export type AST = | ASTTCall | ASTTOut | ASTTForEach + | ASTTFor | ASTTKey | ASTComponent | ASTSlot @@ -230,6 +240,7 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null { return ( parseTDebugLog(node, ctx) || parseTForEach(node, ctx) || + parseTFor(node, ctx) || parseTIf(node, ctx) || parseTPortal(node, ctx) || parseTCall(node, ctx) || @@ -531,6 +542,36 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null { }; } +function parseTFor(node: Element, ctx: ParsingContext): AST | null { + if (!node.hasAttribute("t-for")) { + return null; + } + const binding = node.getAttribute("t-for")!; + node.removeAttribute("t-for"); + const iterable = node.getAttribute("t-of") || ""; + node.removeAttribute("t-of"); + const key = node.getAttribute("t-key"); + if (!key) { + throw new OwlError( + `"Directive t-for should always be used with a t-key!" (expression: t-for="${binding}" t-of="${iterable}")` + ); + } + node.removeAttribute("t-key"); + const body = parseNode(node, ctx); + + if (!body) { + return null; + } + + return { + type: ASTType.TFor, + iterable, + binding, + body, + key, + }; +} + function parseTKey(node: Element, ctx: ParsingContext): AST | null { if (!node.hasAttribute("t-key")) { return null; diff --git a/tests/compiler/__snapshots__/t_for.test.ts.snap b/tests/compiler/__snapshots__/t_for.test.ts.snap new file mode 100644 index 000000000..fe6d391a9 --- /dev/null +++ b/tests/compiler/__snapshots__/t_for.test.ts.snap @@ -0,0 +1,670 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`t-for destructuring array items 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block1 = []; + let i1 = 0; + for ([ctx['key'],ctx['value']] of Object.entries({a:1,b:2})) { + const key1 = ctx['key']; + const b3 = text(\`(\`); + const b4 = text(ctx['key']); + const b5 = text(\`: \`); + const b6 = text(ctx['value']); + const b7 = text(\`)\`); + c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7]), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`t-for destructuring array items: rest 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block1 = []; + let i1 = 0; + for ([ctx['head'],...ctx['tail']] of [[1,2,3],[4,5,6]]) { + const key1 = ctx['head']; + const b3 = text(\`(\`); + const b4 = text(ctx['head']); + const b5 = text(\`;\`); + const b6 = text(ctx['tail']); + const b7 = text(\`)\`); + c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7]), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`t-for destructuring object items 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block1 = []; + let i1 = 0; + for ({k:ctx['k'],v:ctx['v']} of [{k:'a',v:1},{k:'b',v:2}]) { + const key1 = ctx['k']; + const b3 = text(\`(\`); + const b4 = text(ctx['k']); + const b5 = text(\`: \`); + const b6 = text(ctx['v']); + const b7 = text(\`)\`); + c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7]), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`t-for destructuring object items: rest 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block1 = []; + let i1 = 0; + for ({k:ctx['k'],v:ctx['v']} of [{k:'a',v:1},{k:'b',v:2}]) { + const key1 = ctx['k']; + const b3 = text(\`(\`); + const b4 = text(ctx['k']); + const b5 = text(\`: \`); + const b6 = text(ctx['v']); + const b7 = text(\`)\`); + c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7]), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`t-for does not pollute the rendering context 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['item'] of [1]) { + const key1 = ctx['item']; + c_block2[i1] = withKey(text(ctx['item']), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`t-for iterate on items (on a element node) 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + let block1 = createBlock(\`
\`); + let block3 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['item'] of [1,2]) { + const key1 = ctx['item']; + let txt1 = ctx['item']; + c_block2[i1] = withKey(block3([txt1]), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`t-for iterate, Map param 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block1 = []; + let i1 = 0; + for ([ctx['key'],ctx['value']] of ctx['map']) { + const key1 = ctx['key']; + const b3 = text(\` [\`); + const b4 = text(ctx['key']); + const b5 = text(\`: \`); + const b6 = text(ctx['value']); + const b7 = text(\`] \`); + c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7]), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`t-for iterate, Set param 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block1 = []; + let i1 = 0; + for (ctx['item'] of ctx['set']) { + const key1 = ctx['item']; + c_block1[i1] = withKey(text(ctx['item']), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`t-for iterate, generator param 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block1 = []; + let i1 = 0; + for (ctx['item'] of ctx['gen']()) { + const key1 = ctx['item']; + c_block1[i1] = withKey(text(ctx['item']), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`t-for iterate, iterable param 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block1 = []; + let i1 = 0; + for (ctx['item'] of ctx['map'].values()) { + const key1 = ctx['item']; + c_block1[i1] = withKey(text(ctx['item']), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`t-for nested destructuring 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block1 = []; + let i1 = 0; + for ([ctx['key'],{left:ctx['left'],right:ctx['right']}] of Object.entries(ctx['obj'])) { + const key1 = ctx['key']; + const b3 = text(\`(\`); + const b4 = text(ctx['key']); + const b5 = text(\`: [\`); + const b6 = text(ctx['left']); + const b7 = text(\`, \`); + const b8 = text(ctx['right']); + const b9 = text(\`])\`); + c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`t-for simple iteration (in a node) 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['item'] of [3,2,1]) { + const key1 = ctx['item']; + c_block2[i1] = withKey(text(ctx['item']), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`t-for simple iteration 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block1 = []; + let i1 = 0; + for (ctx['item'] of [3,2,1]) { + const key1 = ctx['item']; + c_block1[i1] = withKey(text(ctx['item']), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`t-for simple iteration with two nodes inside 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + let block3 = createBlock(\`a\`); + let block4 = createBlock(\`b\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block1 = []; + let i1 = 0; + for (ctx['item'] of [3,2,1]) { + const key1 = ctx['item']; + let txt1 = ctx['item']; + const b3 = block3([txt1]); + let txt2 = ctx['item']; + const b4 = block4([txt2]); + c_block1[i1] = withKey(multi([b3, b4]), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`t-for t-call with body in t-for in t-for 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { isBoundary, withDefault, setContextValue, withKey } = helpers; + const callTemplate_1 = app.getTemplate(\`sub\`); + + let block1 = createBlock(\`
[][][]
\`); + let block6 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + ctx[isBoundary] = 1 + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['a'] of ctx['numbers']) { + const key1 = ctx['a']; + ctx = Object.create(ctx); + const c_block4 = []; + let i2 = 0; + for (ctx['b'] of ctx['letters']) { + const key2 = ctx['b']; + ctx = Object.create(ctx); + ctx[isBoundary] = 1; + setContextValue(ctx, \\"c\\", 'x'+'_'+ctx['a']+'_'+ctx['b']); + c_block4[i2] = withKey(callTemplate_1.call(this, ctx, node, key + \`__1__\${key1}__\${key2}\`), key2); + ctx = ctx.__proto__; + i2++; + } + ctx = ctx.__proto__; + const b4 = list(c_block4); + let txt1 = ctx['c']; + const b6 = block6([txt1]); + c_block2[i1] = withKey(multi([b4, b6]), key1); + i1++; + } + ctx = ctx.__proto__; + const b2 = list(c_block2); + let txt2 = ctx['a']; + let txt3 = ctx['b']; + let txt4 = ctx['c']; + return block1([txt2, txt3, txt4], [b2]); + } +}" +`; + +exports[`t-for t-call with body in t-for in t-for 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + const b2 = text(\` [\`); + const b3 = text(ctx['a']); + const b4 = text(\`] [\`); + const b5 = text(ctx['b']); + const b6 = text(\`] [\`); + const b7 = text(ctx['c']); + const b8 = text(\`] \`); + return multi([b2, b3, b4, b5, b6, b7, b8]); + } +}" +`; + +exports[`t-for t-call without body in t-for in t-for 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + const callTemplate_1 = app.getTemplate(\`sub\`); + + let block1 = createBlock(\`
[][][]
\`); + let block6 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['a'] of ctx['numbers']) { + const key1 = ctx['a']; + ctx = Object.create(ctx); + const c_block4 = []; + let i2 = 0; + for (ctx['b'] of ctx['letters']) { + const key2 = ctx['b']; + c_block4[i2] = withKey(callTemplate_1.call(this, ctx, node, key + \`__1__\${key1}__\${key2}\`), key2); + i2++; + } + ctx = ctx.__proto__; + const b4 = list(c_block4); + let txt1 = ctx['c']; + const b6 = block6([txt1]); + c_block2[i1] = withKey(multi([b4, b6]), key1); + i1++; + } + ctx = ctx.__proto__; + const b2 = list(c_block2); + let txt2 = ctx['a']; + let txt3 = ctx['b']; + let txt4 = ctx['c']; + return block1([txt2, txt3, txt4], [b2]); + } +}" +`; + +exports[`t-for t-call without body in t-for in t-for 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { isBoundary, withDefault, setContextValue } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + ctx[isBoundary] = 1 + setContextValue(ctx, \\"c\\", 'x'+'_'+ctx['a']+'_'+ctx['b']); + const b2 = text(\` [\`); + const b3 = text(ctx['a']); + const b4 = text(\`] [\`); + const b5 = text(ctx['b']); + const b6 = text(\`] [\`); + const b7 = text(ctx['c']); + const b8 = text(\`] \`); + return multi([b2, b3, b4, b5, b6, b7, b8]); + } +}" +`; + +exports[`t-for t-for in t-for 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['number'] of ctx['numbers']) { + const key1 = ctx['number']; + ctx = Object.create(ctx); + const c_block3 = []; + let i2 = 0; + for (ctx['letter'] of ctx['letters']) { + const key2 = ctx['letter']; + const b5 = text(\` [\`); + const b6 = text(ctx['number']); + const b7 = text(ctx['letter']); + const b8 = text(\`] \`); + c_block3[i2] = withKey(multi([b5, b6, b7, b8]), key2); + i2++; + } + ctx = ctx.__proto__; + c_block2[i1] = withKey(list(c_block3), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`t-for t-for in t-foreach 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { prepareList, withKey } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['numbers']);; + for (let i1 = 0; i1 < l_block2; i1++) { + ctx[\`number\`] = v_block2[i1]; + const key1 = ctx['number']; + ctx = Object.create(ctx); + const c_block3 = []; + let i2 = 0; + for (ctx['letter'] of ctx['letters']) { + const key2 = ctx['letter']; + const b5 = text(\` [\`); + const b6 = text(ctx['number']); + const b7 = text(ctx['letter']); + const b8 = text(\`] \`); + c_block3[i2] = withKey(multi([b5, b6, b7, b8]), key2); + i2++; + } + ctx = ctx.__proto__; + c_block2[i1] = withKey(list(c_block3), key1); + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`t-for t-for with t-if inside (no external node) 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + let block3 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block1 = []; + let i1 = 0; + for ({id:ctx['id'],text:ctx['text']} of ctx['elems']) { + const key1 = ctx['id']; + let b3; + if (ctx['id']<3) { + let txt1 = ctx['text']; + b3 = block3([txt1]); + } + c_block1[i1] = withKey(multi([b3]), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`t-for t-for with t-if inside 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + let block1 = createBlock(\`
\`); + let block4 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for ({id:ctx['id'],text:ctx['text']} of ctx['elems']) { + const key1 = ctx['id']; + let b4; + if (ctx['id']<3) { + let txt1 = ctx['text']; + b4 = block4([txt1]); + } + c_block2[i1] = withKey(multi([b4]), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`t-for t-foreach in t-for 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { prepareList, withKey } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['number'] of ctx['numbers']) { + const key1 = ctx['number']; + ctx = Object.create(ctx); + const [k_block3, v_block3, l_block3, c_block3] = prepareList(ctx['letters']);; + for (let i2 = 0; i2 < l_block3; i2++) { + ctx[\`letter\`] = v_block3[i2]; + const key2 = ctx['letter']; + const b5 = text(\` [\`); + const b6 = text(ctx['number']); + const b7 = text(ctx['letter']); + const b8 = text(\`] \`); + c_block3[i2] = withKey(multi([b5, b6, b7, b8]), key2); + } + ctx = ctx.__proto__; + c_block2[i1] = withKey(list(c_block3), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`t-for t-key on t-for 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + let block1 = createBlock(\`
\`); + let block3 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['thing'] of ctx['things']) { + const key1 = ctx['thing']; + c_block2[i1] = withKey(block3(), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`t-for throws error if invalid loop expression 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + + let block1 = createBlock(\`
\`); + let block3 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['item'] of ctx['abc']) { + const key1 = ctx['item']; + const tKey_1 = ctx['item']; + c_block2[i1] = withKey(block3(), tKey_1 + key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; diff --git a/tests/compiler/__snapshots__/t_set.test.ts.snap b/tests/compiler/__snapshots__/t_set.test.ts.snap index aec7b5b3d..15f8321fe 100644 --- a/tests/compiler/__snapshots__/t_set.test.ts.snap +++ b/tests/compiler/__snapshots__/t_set.test.ts.snap @@ -335,6 +335,99 @@ exports[`t-set t-set evaluates an expression only once 1`] = ` }" `; +exports[`t-set t-set outside modified in t-for 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { isBoundary, withDefault, setContextValue, withKey } = helpers; + + let block1 = createBlock(\`

EndLoop:

\`); + let block3 = createBlock(\`

InLoop:

\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + ctx[isBoundary] = 1 + setContextValue(ctx, \\"iter\\", 0); + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['val'] of ['a','b']) { + const key1 = ctx['val']; + let txt1 = ctx['iter']; + c_block2[i1] = withKey(block3([txt1]), key1); + setContextValue(ctx, \\"iter\\", ctx['iter']+1); + i1++; + } + ctx = ctx.__proto__; + const b2 = list(c_block2); + let txt2 = ctx['iter']; + return block1([txt2], [b2]); + } +}" +`; + +exports[`t-set t-set outside modified in t-for increment-after operator 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { isBoundary, withDefault, setContextValue, withKey } = helpers; + + let block1 = createBlock(\`

EndLoop:

\`); + let block3 = createBlock(\`

InLoop:

\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + ctx[isBoundary] = 1 + setContextValue(ctx, \\"iter\\", 0); + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['val'] of ['a','b']) { + const key1 = ctx['val']; + let txt1 = ctx['iter']; + c_block2[i1] = withKey(block3([txt1]), key1); + setContextValue(ctx, \\"iter\\", ctx['iter']++); + i1++; + } + ctx = ctx.__proto__; + const b2 = list(c_block2); + let txt2 = ctx['iter']; + return block1([txt2], [b2]); + } +}" +`; + +exports[`t-set t-set outside modified in t-for increment-before operator 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { isBoundary, withDefault, setContextValue, withKey } = helpers; + + let block1 = createBlock(\`

EndLoop:

\`); + let block3 = createBlock(\`

InLoop:

\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + ctx[isBoundary] = 1 + setContextValue(ctx, \\"iter\\", 0); + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['val'] of ['a','b']) { + const key1 = ctx['val']; + let txt1 = ctx['iter']; + c_block2[i1] = withKey(block3([txt1]), key1); + setContextValue(ctx, \\"iter\\", ++ctx['iter']); + i1++; + } + ctx = ctx.__proto__; + const b2 = list(c_block2); + let txt2 = ctx['iter']; + return block1([txt2], [b2]); + } +}" +`; + exports[`t-set t-set outside modified in t-foreach 1`] = ` "function anonymous(app, bdom, helpers ) { @@ -454,6 +547,35 @@ exports[`t-set t-set should reuse variable if possible 1`] = ` }" `; +exports[`t-set t-set should reuse variable if possible: for..of 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { isBoundary, withDefault, setContextValue, withKey } = helpers; + + let block1 = createBlock(\`
\`); + let block3 = createBlock(\`
v
\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + ctx[isBoundary] = 1 + setContextValue(ctx, \\"v\\", 1); + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['elem'] of ctx['list']) { + const key1 = ctx['elem_index']; + let txt1 = ctx['v']; + setContextValue(ctx, \\"v\\", ctx['elem']); + c_block2[i1] = withKey(block3([txt1]), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + exports[`t-set t-set with content and sub t-esc 1`] = ` "function anonymous(app, bdom, helpers ) { diff --git a/tests/compiler/t_for.test.ts b/tests/compiler/t_for.test.ts new file mode 100644 index 000000000..34e6d825f --- /dev/null +++ b/tests/compiler/t_for.test.ts @@ -0,0 +1,317 @@ +import { + renderToBdom, + renderToString, + snapshotEverything, + TestContext, + makeTestFixture, +} from "../helpers"; +import { mount, patch } from "../../src/runtime/blockdom"; + +snapshotEverything(); + +// ----------------------------------------------------------------------------- +// t-for +// ----------------------------------------------------------------------------- + +describe("t-for", () => { + test("simple iteration", () => { + const template = ``; + expect(renderToString(template)).toBe("321"); + }); + + test("simple iteration with two nodes inside", () => { + const template = ` + + a + b + `; + const expected = + "a3b3a2b2a1b1"; + expect(renderToString(template)).toBe(expected); + }); + + test("destructuring array items", () => { + const template = `(: )`; + expect(renderToString(template)).toBe("(a: 1)(b: 2)"); + }); + + test("destructuring array items: rest", () => { + const template = `(;)`; + expect(renderToString(template)).toBe("(1;2,3)(4;5,6)"); + }); + + test("destructuring object items", () => { + const template = `(: )`; + expect(renderToString(template)).toBe("(a: 1)(b: 2)"); + }); + + test("destructuring object items: rest", () => { + const template = `(: )`; + expect(renderToString(template)).toBe("(a: 1)(b: 2)"); + }); + + test("nested destructuring", () => { + const template = `(: [, ])`; + expect( + renderToString(template, { + obj: { + a: { left: 1, right: 2 }, + b: { left: 3, right: 4 }, + }, + }) + ).toBe("(a: [1, 2])(b: [3, 4])"); + }); + + test("t-key on t-for", async () => { + const template = ` +
+ + + +
`; + + const fixture = makeTestFixture(); + + const vnode1 = renderToBdom(template, { things: [1, 2] }); + mount(vnode1, fixture); + let elm = fixture; + expect(elm.innerHTML).toBe("
"); + const first = elm.querySelectorAll("span")[0]; + const second = elm.querySelectorAll("span")[1]; + + const vnode2 = renderToBdom(template, { things: [2, 1] }); + patch(vnode1, vnode2); + + expect(elm.innerHTML).toBe("
"); + expect(first).toBe(elm.querySelectorAll("span")[1]); + expect(second).toBe(elm.querySelectorAll("span")[0]); + }); + + test("simple iteration (in a node)", () => { + const template = ` +
+ +
`; + expect(renderToString(template)).toBe("
321
"); + }); + + test("iterate on items (on a element node)", () => { + const template = ` +
+ +
`; + const expected = `
12
`; + expect(renderToString(template)).toBe(expected); + }); + + test("iterate, Map param", () => { + const template = ` + + [: ] + `; + const expected = ` [a: 1] [b: 2] [c: 3] `; + const context = { + map: new Map([ + ["a", 1], + ["b", 2], + ["c", 3], + ]), + }; + expect(renderToString(template, context)).toBe(expected); + }); + + test("iterate, Set param", () => { + const template = ` + + + `; + const expected = `123`; + const context = { set: new Set([1, 2, 3]) }; + expect(renderToString(template, context)).toBe(expected); + }); + + test("iterate, iterable param", () => { + const template = ` + + + `; + const expected = `123`; + 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 = `123`; + const context = { + *gen() { + yield 1; + yield 2; + yield 3; + }, + }; + expect(renderToString(template, context)).toBe(expected); + }); + + test("does not pollute the rendering context", () => { + const template = ` +
+ +
`; + const context = { __owl__: {} }; + renderToString(template, context); + expect(Object.keys(context)).toEqual(["__owl__"]); + }); + + test("t-for in t-for", () => { + const template = ` +
+ + + [] + + +
`; + + const context = { numbers: [1, 2, 3], letters: ["a", "b"] }; + const expected = "
[1a] [1b] [2a] [2b] [3a] [3b]
"; + expect(renderToString(template, context)).toBe(expected); + }); + + test("t-for in t-foreach", () => { + const template = ` +
+ + + [] + + +
`; + + const context = { numbers: [1, 2, 3], letters: ["a", "b"] }; + const expected = "
[1a] [1b] [2a] [2b] [3a] [3b]
"; + expect(renderToString(template, context)).toBe(expected); + }); + + test("t-foreach in t-for", () => { + const template = ` +
+ + + [] + + +
`; + + const context = { numbers: [1, 2, 3], letters: ["a", "b"] }; + const expected = "
[1a] [1b] [2a] [2b] [3a] [3b]
"; + expect(renderToString(template, context)).toBe(expected); + }); + + test("t-call without body in t-for in t-for", () => { + const context = new TestContext(); + const sub = ` + + + [] + [] + [] + `; + + const main = ` +
+ + + + + + + [][][] +
`; + + context.addTemplate("sub", sub); + context.addTemplate("main", main); + + const ctx = { numbers: [1, 2, 3], letters: ["a", "b"] }; + const expected = + "
[1] [a] [x_1_a] [1] [b] [x_1_b] [2] [a] [x_2_a] [2] [b] [x_2_b] [3] [a] [x_3_a] [3] [b] [x_3_b] [][][]
"; + expect(context.renderToString("main", ctx)).toBe(expected); + }); + + test("t-call with body in t-for in t-for", () => { + const context = new TestContext(); + const sub = ` + + [] + [] + [] + `; + + const main = ` +
+ + + + + + + + + [][][] +
`; + + context.addTemplate("sub", sub); + context.addTemplate("main", main); + + const ctx = { numbers: [1, 2, 3], letters: ["a", "b"] }; + const expected = + "
[1] [a] [x_1_a] [1] [b] [x_1_b] [2] [a] [x_2_a] [2] [b] [x_2_b] [3] [a] [x_3_a] [3] [b] [x_3_b] [][][]
"; + expect(context.renderToString("main", ctx)).toBe(expected); + }); + + test("throws error if invalid loop expression", () => { + const test = `
`; + expect(() => renderToString(test)).toThrow("ctx.abc is not iterable"); + }); + + test("t-for with t-if inside", () => { + const template = ` +
+ + + +
`; + const ctx = { + elems: [ + { id: 1, text: "a" }, + { id: 2, text: "b" }, + { id: 3, text: "c" }, + ], + }; + expect(renderToString(template, ctx)).toBe("
ab
"); + }); + + test("t-for with t-if inside (no external node)", () => { + const template = ` + + + `; + const ctx = { + elems: [ + { id: 1, text: "a" }, + { id: 2, text: "b" }, + { id: 3, text: "c" }, + ], + }; + expect(renderToString(template, ctx)).toBe("ab"); + }); +}); diff --git a/tests/compiler/t_set.test.ts b/tests/compiler/t_set.test.ts index 030a15b41..7722fbaf3 100644 --- a/tests/compiler/t_set.test.ts +++ b/tests/compiler/t_set.test.ts @@ -122,6 +122,19 @@ describe("t-set", () => { expect(renderToString(template, { list: ["a", "b"] })).toBe(expected); }); + test("t-set should reuse variable if possible: for..of", () => { + const template = ` +
+ +
+ v + +
+
`; + const expected = "
v1
va
"; + expect(renderToString(template, { list: ["a", "b"] })).toBe(expected); + }); + test("t-set with content and sub t-esc", () => { const template = `
@@ -232,6 +245,22 @@ describe("t-set", () => { ); }); + test("t-set outside modified in t-for", async () => { + const template = ` +
+ + +

InLoop:

+ + +

EndLoop:

+
+ `; + expect(renderToString(template)).toBe( + "

InLoop: 0

InLoop: 1

EndLoop: 2

" + ); + }); + test("t-set outside modified in t-foreach increment-after operator", async () => { const template = `
@@ -248,6 +277,22 @@ describe("t-set", () => { ); }); + test("t-set outside modified in t-for increment-after operator", async () => { + const template = ` +
+ + +

InLoop:

+ + +

EndLoop:

+
+ `; + expect(renderToString(template)).toBe( + "

InLoop: 0

InLoop: 0

EndLoop: 0

" + ); + }); + test("t-set outside modified in t-foreach increment-before operator", async () => { const template = `
@@ -264,6 +309,22 @@ describe("t-set", () => { ); }); + test("t-set outside modified in t-for increment-before operator", async () => { + const template = ` +
+ + +

InLoop:

+ + +

EndLoop:

+
+ `; + expect(renderToString(template)).toBe( + "

InLoop: 0

InLoop: 1

EndLoop: 0

" + ); + }); + test("t-set can't alter from within callee", async () => { const context = new TestContext(); const sub = `
`; diff --git a/tests/components/__snapshots__/t_for.test.ts.snap b/tests/components/__snapshots__/t_for.test.ts.snap new file mode 100644 index 000000000..15591e69f --- /dev/null +++ b/tests/components/__snapshots__/t_for.test.ts.snap @@ -0,0 +1,493 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`list of components components in a node in a t-for 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + const comp1 = app.createComponent(\`Child\`, true, false, false, [\\"item\\"]); + + let block1 = createBlock(\`
\`); + let block3 = createBlock(\`
  • \`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['item'] of ctx['items']) { + const key1 = 'li_'+ctx['item']; + const b4 = comp1({item: ctx['item']}, key + \`__1__\${key1}\`, node, this, null); + c_block2[i1] = withKey(block3([], [b4]), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`list of components components in a node in a t-for 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`
    \`); + + return function template(ctx, node, key = \\"\\") { + let txt1 = ctx['props'].item; + return block1([txt1]); + } +}" +`; + +exports[`list of components crash on duplicate key in dev mode 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { OwlError, withKey } = helpers; + const comp1 = app.createComponent(\`Child\`, true, false, false, []); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const keys1 = new Set(); + const c_block1 = []; + let i1 = 0; + for (ctx['item'] of [1,2]) { + const key1 = 'child'; + if (keys1.has(String(key1))) { throw new OwlError(\`Got duplicate key in t-for: \${key1}\`)} + keys1.add(String(key1)); + const props1 = {}; + helpers.validateProps(\`Child\`, props1, this); + c_block1[i1] = withKey(comp1(props1, key + \`__1__\${key1}\`, node, this, null), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`list of components crash on duplicate key in dev mode 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + return text(\`\`); + } +}" +`; + +exports[`list of components crash when using object as keys that serialize to the same string 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { OwlError, withKey } = helpers; + const comp1 = app.createComponent(\`Child\`, true, false, false, []); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const keys1 = new Set(); + const c_block1 = []; + let i1 = 0; + for (ctx['item'] of [{},{}]) { + const key1 = ctx['item']; + if (keys1.has(String(key1))) { throw new OwlError(\`Got duplicate key in t-for: \${key1}\`)} + keys1.add(String(key1)); + const props1 = {}; + helpers.validateProps(\`Child\`, props1, this); + c_block1[i1] = withKey(comp1(props1, key + \`__1__\${key1}\`, node, this, null), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`list of components crash when using object as keys that serialize to the same string 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + return text(\`\`); + } +}" +`; + +exports[`list of components list of sub components inside other nodes 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + const comp1 = app.createComponent(\`SubComponent\`, true, false, false, []); + + let block1 = createBlock(\`
    \`); + let block3 = createBlock(\`
    \`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['blip'] of ctx['state'].blips) { + const key1 = ctx['blip'].id; + const b4 = comp1({}, key + \`__1__\${key1}\`, node, this, null); + c_block2[i1] = withKey(block3([], [b4]), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`list of components list of sub components inside other nodes 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`asdf\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; + +exports[`list of components order is correct when slots are not of same type 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { capture, markRaw } = helpers; + const comp1 = app.createComponent(\`Child\`, true, true, false, []); + + let block2 = createBlock(\`
    A
    \`); + + function slot1(ctx, node, key = \\"\\") { + let b2; + if (!ctx['state'].active) { + b2 = block2(); + } + return multi([b2]); + } + + function slot2(ctx, node, key = \\"\\") { + return text(\`B\`); + } + + function slot3(ctx, node, key = \\"\\") { + return text(\`C\`); + } + + return function template(ctx, node, key = \\"\\") { + const ctx1 = capture(ctx); + return comp1({slots: markRaw({'a': {__render: slot1.bind(this), __ctx: ctx1, active: !ctx['state'].active}, 'b': {__render: slot2.bind(this), __ctx: ctx1, active: true}, 'c': {__render: slot3.bind(this), __ctx: ctx1, active: ctx['state'].active}})}, key + \`__1\`, node, this, null); + } +}" +`; + +exports[`list of components order is correct when slots are not of same type 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { callSlot, withKey } = helpers; + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block1 = []; + let i1 = 0; + for (ctx['slotName'] of ctx['slotNames']) { + const key1 = ctx['slotName']; + const slot1 = (ctx['slotName']); + c_block1[i1] = withKey(toggler(slot1, callSlot(ctx, node, key1 + \`__1__\${key1}\`, slot1, true, {})), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`list of components reconciliation alg works for t-for in t-for 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + const comp1 = app.createComponent(\`Child\`, true, false, false, [\\"blip\\"]); + + let block1 = createBlock(\`
    \`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['section'] of ctx['state'].s) { + const key1 = ctx['section']; + ctx = Object.create(ctx); + const c_block3 = []; + let i2 = 0; + for (ctx['blip'] of ctx['section'].blips) { + const key2 = ctx['blip']; + c_block3[i2] = withKey(comp1({blip: ctx['blip']}, key + \`__1__\${key1}__\${key2}\`, node, this, null), key2); + i2++; + } + ctx = ctx.__proto__; + c_block2[i1] = withKey(list(c_block3), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`list of components reconciliation alg works for t-for in t-for 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`
    \`); + + return function template(ctx, node, key = \\"\\") { + let txt1 = ctx['props'].blip; + return block1([txt1]); + } +}" +`; + +exports[`list of components reconciliation alg works for t-for in t-for, 2 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + const comp1 = app.createComponent(\`Child\`, true, false, false, [\\"row\\",\\"col\\"]); + + let block1 = createBlock(\`
    \`); + let block3 = createBlock(\`

    \`); + let block5 = createBlock(\`

    \`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['row'] of ctx['state'].rows) { + const key1 = ctx['row']; + ctx = Object.create(ctx); + const c_block4 = []; + let i2 = 0; + for (ctx['col'] of ctx['state'].cols) { + const key2 = ctx['col']; + const b6 = comp1({row: ctx['row'],col: ctx['col']}, key + \`__1__\${key1}__\${key2}\`, node, this, null); + c_block4[i2] = withKey(block5([], [b6]), key2); + i2++; + } + ctx = ctx.__proto__; + const b4 = list(c_block4); + c_block2[i1] = withKey(block3([], [b4]), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`list of components reconciliation alg works for t-for in t-for, 2 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`
    \`); + + return function template(ctx, node, key = \\"\\") { + let txt1 = ctx['props'].row+'_'+ctx['props'].col; + return block1([txt1]); + } +}" +`; + +exports[`list of components simple list 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + const comp1 = app.createComponent(\`Child\`, true, false, false, [\\"value\\"]); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block1 = []; + let i1 = 0; + for (ctx['elem'] of ctx['state'].elems) { + const key1 = ctx['elem'].id; + c_block1[i1] = withKey(comp1({value: ctx['elem'].value}, key + \`__1__\${key1}\`, node, this, null), key1); + i1++; + } + return list(c_block1); + } +}" +`; + +exports[`list of components simple list 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + let txt1 = ctx['props'].value; + return block1([txt1]); + } +}" +`; + +exports[`list of components sub components rendered in a loop 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + const comp1 = app.createComponent(\`Child\`, true, false, false, [\\"n\\"]); + + let block1 = createBlock(\`
    \`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['number'] of ctx['state'].numbers) { + const key1 = ctx['number']; + c_block2[i1] = withKey(comp1({n: ctx['number']}, key + \`__1__\${key1}\`, node, this, null), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`list of components sub components rendered in a loop 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`

    \`); + + return function template(ctx, node, key = \\"\\") { + let txt1 = ctx['props'].n; + return block1([txt1]); + } +}" +`; + +exports[`list of components sub components with some state rendered in a loop 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + const comp1 = app.createComponent(\`Child\`, true, false, false, []); + + let block1 = createBlock(\`
    \`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['number'] of ctx['state'].numbers) { + const key1 = ctx['number']; + c_block2[i1] = withKey(comp1({}, key + \`__1__\${key1}\`, node, this, null), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`list of components sub components with some state rendered in a loop 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`

    \`); + + return function template(ctx, node, key = \\"\\") { + let txt1 = ctx['state'].n; + return block1([txt1]); + } +}" +`; + +exports[`list of components switch component position 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + const comp1 = app.createComponent(\`Child\`, true, false, false, [\\"key\\"]); + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['c'] of ctx['clist']) { + const key1 = ctx['c']; + c_block2[i1] = withKey(comp1({key: ctx['c']}, key + \`__1__\${key1}\`, node, this, null), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`list of components switch component position 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`
    \`); + + return function template(ctx, node, key = \\"\\") { + let txt1 = ctx['props'].key; + return block1([txt1]); + } +}" +`; + +exports[`list of components t-for with t-component, and update 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { withKey } = helpers; + const comp1 = app.createComponent(\`Child\`, true, false, false, [\\"val\\"]); + + let block1 = createBlock(\`
    \`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const c_block2 = []; + let i1 = 0; + for (ctx['n'] of [0,1]) { + const key1 = ctx['n']; + c_block2[i1] = withKey(comp1({val: ctx['n']}, key + \`__1__\${key1}\`, node, this, null), key1); + i1++; + } + const b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`list of components t-for with t-component, and update 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + let txt1 = ctx['state'].val; + let txt2 = ctx['props'].val; + return block1([txt1, txt2]); + } +}" +`; diff --git a/tests/components/t_for.test.ts b/tests/components/t_for.test.ts new file mode 100644 index 000000000..2c469542d --- /dev/null +++ b/tests/components/t_for.test.ts @@ -0,0 +1,391 @@ +import { App, Component, mount, onMounted, useState, xml } from "../../src/index"; +import { + makeTestFixture, + nextAppError, + nextTick, + snapshotEverything, + useLogLifecycle, +} from "../helpers"; + +snapshotEverything(); + +let originalconsoleWarn = console.warn; +let mockConsoleWarn: any; + +let fixture: HTMLElement; + +beforeEach(() => { + fixture = makeTestFixture(); + mockConsoleWarn = jest.fn(() => {}); + console.warn = mockConsoleWarn; +}); + +afterEach(() => { + console.warn = originalconsoleWarn; +}); + +describe("list of components", () => { + test("simple list", async () => { + class Child extends Component { + static template = xml``; + } + + class Parent extends Component { + static template = xml` + + + `; + static components = { Child }; + + state = useState({ + elems: [ + { id: 1, value: "a" }, + { id: 2, value: "b" }, + ], + }); + } + + const parent = await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("ab"); + parent.state.elems.push({ id: 4, value: "d" }); + await nextTick(); + expect(fixture.innerHTML).toBe("abd"); + + parent.state.elems.pop(); + + await nextTick(); + expect(fixture.innerHTML).toBe("ab"); + }); + + test("components in a node in a t-for ", async () => { + class Child extends Component { + static template = xml`
    `; + setup() { + useLogLifecycle(); + } + } + + class Parent extends Component { + static template = xml` +
    +
      + +
    • + +
    • +
      +
    +
    `; + static components = { Child }; + + setup() { + useLogLifecycle(); + } + + get items() { + return [1, 2]; + } + } + + await mount(Parent, fixture); + expect(fixture.innerHTML).toBe( + "
    • 1
    • 2
    " + ); + expect([ + "Parent:setup", + "Parent:willStart", + "Parent:willRender", + "Child:setup", + "Child:willStart", + "Child:setup", + "Child:willStart", + "Parent:rendered", + "Child:willRender", + "Child:rendered", + "Child:willRender", + "Child:rendered", + "Child:mounted", + "Child:mounted", + "Parent:mounted", + ]).toBeLogged(); + }); + + test("reconciliation alg works for t-for in t-for", async () => { + class Child extends Component { + static template = xml`
    `; + } + + class Parent extends Component { + static template = xml` +
    + + + + + +
    `; + static components = { Child }; + state = { s: [{ blips: ["a1", "a2"] }, { blips: ["b1"] }] }; + } + + await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("
    a1
    a2
    b1
    "); + }); + + test("reconciliation alg works for t-for in t-for, 2", async () => { + class Child extends Component { + static template = xml`
    `; + } + + class Parent extends Component { + static template = xml` +
    +

    +

    + +

    +

    +
    `; + static components = { Child }; + state = useState({ rows: [1, 2], cols: ["a", "b"] }); + } + + const parent = await mount(Parent, fixture); + expect(fixture.innerHTML).toBe( + "

    1_a

    1_b

    2_a

    2_b

    " + ); + parent.state.rows = [2, 1]; + await nextTick(); + expect(fixture.innerHTML).toBe( + "

    2_a

    2_b

    1_a

    1_b

    " + ); + }); + + test("sub components rendered in a loop", async () => { + class Child extends Component { + static template = xml`

    `; + } + + class Parent extends Component { + static template = xml` +
    + + + +
    `; + static components = { Child }; + + state = useState({ numbers: [1, 2, 3] }); + } + await mount(Parent, fixture); + + expect(fixture.innerHTML).toBe(`

    1

    2

    3

    `); + }); + + test("sub components with some state rendered in a loop", async () => { + let n = 1; + + class Child extends Component { + static template = xml`

    `; + state: any; + setup() { + this.state = useState({ n }); + n++; + } + } + + class Parent extends Component { + static template = xml` +
    + + + +
    `; + static components = { Child }; + + state = useState({ + numbers: [1, 2, 3], + }); + } + const parent = await mount(Parent, fixture); + + parent.state.numbers = [1, 3]; + await nextTick(); + expect(fixture.innerHTML).toBe(`

    1

    3

    `); + }); + + test("list of sub components inside other nodes", async () => { + // this confuses the patching algorithm... + class SubComponent extends Component { + static template = xml`asdf`; + } + class Parent extends Component { + static template = xml` +
    +
    + +
    +
    `; + static components = { SubComponent }; + state = useState({ + blips: [ + { a: "a", id: 1 }, + { b: "b", id: 2 }, + { c: "c", id: 4 }, + ], + }); + } + const parent = await mount(Parent, fixture); + expect(fixture.innerHTML).toBe( + "
    asdf
    asdf
    asdf
    " + ); + parent.state.blips.splice(0, 1); + await nextTick(); + expect(fixture.innerHTML).toBe( + "
    asdf
    asdf
    " + ); + }); + + test("t-for with t-component, and update", async () => { + class Child extends Component { + static template = xml` + + + + `; + state = useState({ val: "A" }); + setup() { + onMounted(() => { + this.state.val = "B"; + }); + } + } + class Parent extends Component { + static components = { Child }; + static template = xml` +
    + + + +
    `; + } + + await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("
    A0A1
    "); + + await nextTick(); // wait for changes triggered in mounted to be applied + expect(fixture.innerHTML).toBe("
    B0B1
    "); + }); + + test("switch component position", async () => { + const childInstances = []; + class Child extends Component { + static template = xml`
    `; + setup() { + childInstances.push(this); + } + } + + class Parent extends Component { + static components = { Child }; + static template = xml` + + + + `; + + clist = [1, 2]; + } + + const parent = await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("
    1
    2
    "); + parent.clist = [2, 1]; + parent.render(); + await nextTick(); + expect(fixture.innerHTML).toBe("
    2
    1
    "); + expect(childInstances.length).toBe(2); + }); + + test("crash on duplicate key in dev mode", async () => { + const consoleInfo = console.info; + console.info = jest.fn(); + class Child extends Component { + static template = xml``; + } + + class Parent extends Component { + static template = xml` + + + + `; + static components = { Child }; + } + + const app = new App(Parent, { test: true }); + const mountProm = expect(app.mount(fixture)).rejects.toThrow( + "Got duplicate key in t-for: child" + ); + await expect(nextAppError(app)).resolves.toThrow("Got duplicate key in t-for: child"); + await mountProm; + console.info = consoleInfo; + expect(mockConsoleWarn).toBeCalledTimes(1); + }); + + test("crash when using object as keys that serialize to the same string", async () => { + const consoleInfo = console.info; + console.info = jest.fn(); + class Child extends Component { + static template = xml``; + } + + class Parent extends Component { + static template = xml` + + + + `; + static components = { Child }; + } + + const app = new App(Parent, { test: true }); + const mountProm = expect(app.mount(fixture)).rejects.toThrow( + "Got duplicate key in t-for: [object Object]" + ); + await expect(nextAppError(app)).resolves.toThrow("Got duplicate key in t-for: [object Object]"); + await mountProm; + console.info = consoleInfo; + expect(mockConsoleWarn).toBeCalledTimes(1); + }); + + test("order is correct when slots are not of same type", async () => { + class Child extends Component { + static template = xml` + + `; + get slotNames() { + return Object.entries(this.props.slots) + .filter((entry: any) => entry[1].active) + .map((entry) => entry[0]); + } + } + + class Parent extends Component { + static template = xml` + +
    A
    + B + C +
    + `; + static components = { Child }; + state = useState({ active: false }); + } + + const parent = await mount(Parent, fixture); + expect(fixture.textContent).toBe("AB"); + parent.state.active = true; + await nextTick(); + expect(fixture.textContent).toBe("BC"); + }); +});