From 7c04cc425eda449009c926efc2f4974f00991e6d Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Thu, 4 Nov 2021 13:46:49 +0100 Subject: [PATCH] [IMP] qweb: compiler: support t-key on node and component without t-foreach --- src/blockdom/toggler.ts | 4 +- src/qweb/compiler.ts | 39 ++- src/qweb/inline_expressions.ts | 7 +- tests/blockdom/toggler.test.ts | 57 +++++ .../__snapshots__/basics.test.ts.snap | 16 +- .../__snapshots__/concurrency.test.ts.snap | 14 +- .../__snapshots__/t_component.test.ts.snap | 10 +- .../__snapshots__/t_foreach.test.ts.snap | 37 +++ .../__snapshots__/t_key.test.ts.snap | 236 ++++++++++++++++++ tests/components/concurrency.test.ts | 39 +-- tests/components/error_handling.test.ts | 15 +- tests/components/style_class.test.ts | 3 +- tests/components/t_foreach.test.ts | 28 +++ tests/components/t_key.test.ts | 208 +++++++++++++++ tests/misc/portal.test.ts | 3 +- .../qweb/__snapshots__/t_foreach.test.ts.snap | 3 +- tests/qweb/__snapshots__/t_key.test.ts.snap | 35 ++- tests/qweb/__snapshots__/t_ref.test.ts.snap | 7 +- tests/qweb/t_key.test.ts | 25 +- 19 files changed, 722 insertions(+), 64 deletions(-) create mode 100644 tests/components/__snapshots__/t_key.test.ts.snap create mode 100644 tests/components/t_key.test.ts diff --git a/src/blockdom/toggler.ts b/src/blockdom/toggler.ts index 367be569..d31c84ad 100644 --- a/src/blockdom/toggler.ts +++ b/src/blockdom/toggler.ts @@ -43,7 +43,9 @@ class VToggler { } } - beforeRemove() {} + beforeRemove() { + this.child.beforeRemove(); + } remove() { this.child.remove(); diff --git a/src/qweb/compiler.ts b/src/qweb/compiler.ts index b445bae7..15e92dd0 100644 --- a/src/qweb/compiler.ts +++ b/src/qweb/compiler.ts @@ -123,6 +123,7 @@ interface Context { preventRoot?: boolean; isLast?: boolean; translate: boolean; + tKeyExpr: string | null; } function createContext(parentCtx: Context, params?: Partial) { @@ -132,6 +133,7 @@ function createContext(parentCtx: Context, params?: Partial) { index: 0, forceNewBlock: true, translate: parentCtx.translate, + tKeyExpr: null, }, params ); @@ -169,7 +171,6 @@ export class QWebCompiler { nextBlockId = 1; shouldProtectScope: boolean = false; shouldDefineAssign: boolean = false; - shouldDefineKey0: boolean = false; hasSafeContext: boolean | null = null; hasRef: boolean = false; // hasTCall: boolean = false; @@ -212,6 +213,7 @@ export class QWebCompiler { forceNewBlock: false, isLast: true, translate: true, + tKeyExpr: null, }); const code = this.generateCode(); return new Function("bdom, helpers", code) as TemplateFunction; @@ -257,12 +259,22 @@ export class QWebCompiler { insertBlock(expression: string, block: BlockDescription, ctx: Context): string | null { let id: string | null = null; - const blockExpr = block.generateExpr(expression); + let blockExpr = block.generateExpr(expression); + const tKeyExpr = ctx.tKeyExpr; if (block.parentVar) { - this.addLine( - `${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, key${this.target.loopLevel});` - ); - } else if (block.isRoot && !ctx.preventRoot) { + let keyArg = `key${this.target.loopLevel}`; + if (tKeyExpr) { + keyArg = `${tKeyExpr} + ${keyArg}`; + } + this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${keyArg});`); + return id; + } + + if (tKeyExpr) { + blockExpr = `toggler(${tKeyExpr}, ${blockExpr})`; + } + + if (block.isRoot && !ctx.preventRoot) { this.addLine(`return ${blockExpr};`); } else { this.addLine(`let ${block.varName} = ${blockExpr};`); @@ -796,6 +808,13 @@ export class QWebCompiler { } compileTKey(ast: ASTTKey, ctx: Context) { + const tKeyExpr = this.generateId("tKey_"); + this.addLine(`const ${tKeyExpr} = ${compileExpr(ast.expr)};`); + ctx = createContext(ctx, { + tKeyExpr, + block: ctx.block, + index: ctx.index, + }); this.compileAST(ast.content, ctx); } @@ -970,7 +989,11 @@ export class QWebCompiler { propString = propVar; } - let blockArgs = `${expr}, ${propString}, key + \`${key}\`, node, ctx`; + let keyArg = `key + \`${key}\``; + if (ctx.tKeyExpr) { + keyArg = `${ctx.tKeyExpr} + ${keyArg}`; + } + let blockArgs = `${expr}, ${propString}, ${keyArg}, node, ctx`; // slots const hasSlot = !!Object.keys(ast.slots).length; @@ -1007,7 +1030,7 @@ export class QWebCompiler { extraArgs.slots = slotDef; } - if (block && ctx.forceNewBlock === false) { + if (block && (ctx.forceNewBlock === false || ctx.tKeyExpr)) { // todo: check the forcenewblock condition this.insertAnchor(block); } diff --git a/src/qweb/inline_expressions.ts b/src/qweb/inline_expressions.ts index 24a533d8..2474c414 100644 --- a/src/qweb/inline_expressions.ts +++ b/src/qweb/inline_expressions.ts @@ -25,10 +25,9 @@ // Misc types, constants and helpers //------------------------------------------------------------------------------ -const RESERVED_WORDS = - "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split( - "," - ); +const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split( + "," +); const WORD_REPLACEMENT: { [key: string]: string } = Object.assign(Object.create(null), { and: "&&", diff --git a/tests/blockdom/toggler.test.ts b/tests/blockdom/toggler.test.ts index efa114e6..be411a14 100644 --- a/tests/blockdom/toggler.test.ts +++ b/tests/blockdom/toggler.test.ts @@ -54,4 +54,61 @@ describe("togglers", () => { expect(fixture.innerHTML).toBe("

hey

"); expect(fixture.firstChild).not.toBe(textnode); }); + + test("beforeRemove is called", () => { + const block = createBlock("

hey

"); + + function blockOverriden() { + const vn = block(); + vn.beforeRemove = () => { + steps.push("beforeRemove"); + }; + return vn; + } + + const steps: string[] = []; + const tree = toggler("key1", blockOverriden()); + mount(tree, fixture); + expect(fixture.innerHTML).toBe("

hey

"); + patch(tree, toggler("key2", text("foo"))); + expect(fixture.innerHTML).toBe("foo"); + + patch(tree, toggler("key3", blockOverriden()), true); + expect(fixture.innerHTML).toBe("

hey

"); + + patch(tree, toggler("key2", text("foo")), true); + expect(fixture.innerHTML).toBe("foo"); + + expect(steps).toEqual(["beforeRemove"]); + }); + + test("beforeRemove is called when within a block", () => { + const block = createBlock("

"); + const conditionalBlock = createBlock("

hey

"); + + const steps: string[] = []; + + function blockOverriden() { + const vn = conditionalBlock(); + vn.beforeRemove = function () { + steps.push("beforeRemove"); + }; + return vn; + } + const mainBlock = block([], [toggler("key1", blockOverriden())]); + + mount(mainBlock, fixture); + expect(fixture.innerHTML).toBe("

hey

"); + + patch(mainBlock, block([], [])); + expect(fixture.innerHTML).toBe("

"); + + patch(mainBlock, block([], [toggler("key1", blockOverriden())]), true); + expect(fixture.innerHTML).toBe("

hey

"); + + patch(mainBlock, block([], []), true); + expect(fixture.innerHTML).toBe("

"); + + expect(steps).toEqual(["beforeRemove"]); + }); }); diff --git a/tests/components/__snapshots__/basics.test.ts.snap b/tests/components/__snapshots__/basics.test.ts.snap index a42818e9..19d66627 100644 --- a/tests/components/__snapshots__/basics.test.ts.snap +++ b/tests/components/__snapshots__/basics.test.ts.snap @@ -581,7 +581,8 @@ exports[`basics reconciliation alg is not confused in some specific situation 2` return function template(ctx, node, key = \\"\\") { let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx); - let b3 = component(\`Child\`, {}, key + \`__2\`, node, ctx); + const tKey_2 = 4; + let b3 = toggler(tKey_2, component(\`Child\`, {}, tKey_2 + key + \`__3\`, node, ctx)); return block1([], [b2, b3]); } }" @@ -640,8 +641,10 @@ exports[`basics same t-keys in two different places 2`] = ` let block1 = createBlock(\`
\`); return function template(ctx, node, key = \\"\\") { - let b2 = component(\`Child\`, {blip: '1'}, key + \`__1\`, node, ctx); - let b3 = component(\`Child\`, {blip: '2'}, key + \`__2\`, node, ctx); + const tKey_1 = 1; + let b2 = toggler(tKey_1, component(\`Child\`, {blip: '1'}, tKey_1 + key + \`__2\`, node, ctx)); + const tKey_3 = 1; + let b3 = toggler(tKey_3, component(\`Child\`, {blip: '2'}, tKey_3 + key + \`__4\`, node, ctx)); return block1([], [b2, b3]); } }" @@ -889,14 +892,15 @@ exports[`basics t-key on a component with t-if, and a sibling component 2`] = ` let { text, createBlock, list, multi, html, toggler, component } = bdom; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; - let block1 = createBlock(\`
\`); + let block1 = createBlock(\`
\`); return function template(ctx, node, key = \\"\\") { let b2,b3; if (false) { - b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx); + const tKey_1 = 'str'; + b2 = toggler(tKey_1, component(\`Child\`, {}, tKey_1 + key + \`__2\`, node, ctx)); } - b3 = component(\`Child\`, {}, key + \`__2\`, node, ctx); + b3 = component(\`Child\`, {}, key + \`__3\`, node, ctx); return block1([], [b2, b3]); } }" diff --git a/tests/components/__snapshots__/concurrency.test.ts.snap b/tests/components/__snapshots__/concurrency.test.ts.snap index 5d746d91..8e839c8a 100644 --- a/tests/components/__snapshots__/concurrency.test.ts.snap +++ b/tests/components/__snapshots__/concurrency.test.ts.snap @@ -18,7 +18,7 @@ exports[`calling render in destroy 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component } = bdom; - let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, inTCall, shallowEqual, setContextValue } = helpers; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; let block1 = createBlock(\`
\`); @@ -33,7 +33,7 @@ exports[`calling render in destroy 2`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component } = bdom; - let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; return function template(ctx, node, key = \\"\\") { return component(\`C\`, {fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx); @@ -45,10 +45,11 @@ exports[`calling render in destroy 3`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component } = bdom; - let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; return function template(ctx, node, key = \\"\\") { - return component(\`B\`, {fromA: ctx['state']}, key + \`__1\`, node, ctx); + const tKey_1 = ctx['key']; + return toggler(tKey_1, component(\`B\`, {fromA: ctx['state']}, tKey_1 + key + \`__2\`, node, ctx)); } }" `; @@ -1159,12 +1160,13 @@ exports[`two renderings initiated between willPatch and patched 2`] = ` let { text, createBlock, list, multi, html, toggler, component } = bdom; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; - let block1 = createBlock(\`
\`); + let block1 = createBlock(\`
\`); return function template(ctx, node, key = \\"\\") { let b2; if (ctx['state'].flag) { - b2 = component(\`Panel\`, {val: ctx['state'].panel}, key + \`__1\`, node, ctx); + const tKey_1 = 'panel_'+ctx['state'].panel; + b2 = toggler(tKey_1, component(\`Panel\`, {val: ctx['state'].panel}, tKey_1 + key + \`__2\`, node, ctx)); } return block1([], [b2]); } diff --git a/tests/components/__snapshots__/t_component.test.ts.snap b/tests/components/__snapshots__/t_component.test.ts.snap index 247a85ce..90fa231e 100644 --- a/tests/components/__snapshots__/t_component.test.ts.snap +++ b/tests/components/__snapshots__/t_component.test.ts.snap @@ -79,8 +79,9 @@ exports[`t-component can use dynamic components (the class) if given (with diffe let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; return function template(ctx, node, key = \\"\\") { - let Comp2 = ctx['myComponent']; - return toggler(Comp2, component(Comp2, {}, key + \`__1\`, node, ctx)); + const tKey_1 = ctx['state'].child; + let Comp3 = ctx['myComponent']; + return toggler(tKey_1, toggler(Comp3, component(Comp3, {}, tKey_1 + key + \`__2\`, node, ctx))); } }" `; @@ -120,8 +121,9 @@ exports[`t-component can use dynamic components (the class) if given 3`] = ` let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; return function template(ctx, node, key = \\"\\") { - let Comp2 = ctx['myComponent']; - return toggler(Comp2, component(Comp2, {}, key + \`__1\`, node, ctx)); + const tKey_1 = ctx['state'].child; + let Comp3 = ctx['myComponent']; + return toggler(tKey_1, toggler(Comp3, component(Comp3, {}, tKey_1 + key + \`__2\`, node, ctx))); } }" `; diff --git a/tests/components/__snapshots__/t_foreach.test.ts.snap b/tests/components/__snapshots__/t_foreach.test.ts.snap index 60fa7592..8492ed7d 100644 --- a/tests/components/__snapshots__/t_foreach.test.ts.snap +++ b/tests/components/__snapshots__/t_foreach.test.ts.snap @@ -281,6 +281,43 @@ exports[`list of components sub components with some state rendered in a loop 2` }" `; +exports[`list of components switch component position 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['props'].key; + return block1([d1]); + } +}" +`; + +exports[`list of components switch component position 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = 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['clist']); + for (let i1 = 0; i1 < l_block2; i1++) { + ctx[\`c\`] = v_block2[i1]; + let key1 = ctx['c']; + c_block2[i1] = withKey(component(\`Child\`, {key: ctx['c']}, key + \`__1__\${key1}\`, node, ctx), key1); + } + let b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + exports[`list of components t-foreach with t-component, and update 1`] = ` "function anonymous(bdom, helpers ) { diff --git a/tests/components/__snapshots__/t_key.test.ts.snap b/tests/components/__snapshots__/t_key.test.ts.snap new file mode 100644 index 00000000..0202ed22 --- /dev/null +++ b/tests/components/__snapshots__/t_key.test.ts.snap @@ -0,0 +1,236 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`t-key t-foreach with t-key switch component position 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['props'].key; + return block1([d1]); + } +}" +`; + +exports[`t-key t-foreach with t-key switch component position 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = 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['clist']); + for (let i1 = 0; i1 < l_block2; i1++) { + ctx[\`c\`] = v_block2[i1]; + let key1 = ctx['c']; + const tKey_1 = ctx['key1']; + c_block2[i1] = withKey(component(\`Child\`, {key: ctx['c']+ctx['key1']}, tKey_1 + key + \`__2__\${key1}\`, node, ctx), tKey_1 + key1); + } + let b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`t-key t-key on Component 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['props'].key; + return block1([d1]); + } +}" +`; + +exports[`t-key t-key on Component 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + const tKey_1 = ctx['key']; + let b2 = toggler(tKey_1, component(\`Child\`, {key: ctx['key']}, tKey_1 + key + \`__2\`, node, ctx)); + return block1([], [b2]); + } +}" +`; + +exports[`t-key t-key on Component as a function 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['props'].key; + return block1([d1]); + } +}" +`; + +exports[`t-key t-key on Component as a function 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + const tKey_1 = ctx['key']; + let b2 = toggler(tKey_1, component(\`Child\`, {key: ctx['key']}, tKey_1 + key + \`__2\`, node, ctx)); + return block1([], [b2]); + } +}" +`; + +exports[`t-key t-key on multiple Components 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['props'].key; + return block1([d1]); + } +}" +`; + +exports[`t-key t-key on multiple Components 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + const tKey_1 = ctx['key1']; + let b2 = toggler(tKey_1, component(\`Child\`, {key: ctx['key1']}, tKey_1 + key + \`__2\`, node, ctx)); + const tKey_3 = ctx['key2']; + let b3 = toggler(tKey_3, component(\`Child\`, {key: ctx['key2']}, tKey_3 + key + \`__4\`, node, ctx)); + return block1([], [b2, b3]); + } +}" +`; + +exports[`t-key t-key on multiple Components with t-call 1 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['props'].key; + return block1([d1]); + } +}" +`; + +exports[`t-key t-key on multiple Components with t-call 1 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + return function template(ctx, node, key = \\"\\") { + const tKey_1 = ctx['key']; + return toggler(tKey_1, component(\`Child\`, {key: ctx['key']}, tKey_1 + key + \`__2\`, node, ctx)); + } +}" +`; + +exports[`t-key t-key on multiple Components with t-call 1 3`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + const callTemplate_2 = getTemplate(\`calledTemplate\`); + const callTemplate_4 = getTemplate(\`calledTemplate\`); + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + ctx[isBoundary] = 1 + ctx = Object.create(ctx); + ctx[isBoundary] = 1; + setContextValue(ctx, \\"key\\", ctx['key1']); + let b2 = callTemplate_2(ctx, node, key + \`__1\`); + ctx = ctx.__proto__; + ctx = Object.create(ctx); + ctx[isBoundary] = 1; + setContextValue(ctx, \\"key\\", ctx['key2']); + let b3 = callTemplate_4(ctx, node, key + \`__3\`); + return block1([], [b2, b3]); + } +}" +`; + +exports[`t-key t-key on multiple Components with t-call 2 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['props'].key; + return block1([d1]); + } +}" +`; + +exports[`t-key t-key on multiple Components with t-call 2 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + return function template(ctx, node, key = \\"\\") { + const tKey_1 = ctx['key1']; + let b2 = toggler(tKey_1, component(\`Child\`, {key: ctx['key1']}, tKey_1 + key + \`__2\`, node, ctx)); + const tKey_3 = ctx['key2']; + let b3 = toggler(tKey_3, component(\`Child\`, {key: ctx['key2']}, tKey_3 + key + \`__4\`, node, ctx)); + return multi([b2, b3]); + } +}" +`; + +exports[`t-key t-key on multiple Components with t-call 2 3`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + const callTemplate_2 = getTemplate(\`calledTemplate\`); + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + let b2 = callTemplate_2(ctx, node, key + \`__1\`); + return block1([], [b2]); + } +}" +`; diff --git a/tests/components/concurrency.test.ts b/tests/components/concurrency.test.ts index 12a19841..396a65b3 100644 --- a/tests/components/concurrency.test.ts +++ b/tests/components/concurrency.test.ts @@ -2108,7 +2108,7 @@ test("concurrent renderings scenario 16", async () => { // }); // TODO: unskip when t-key is reimplemented properly -test.skip("calling render in destroy", async () => { +test("calling render in destroy", async () => { const steps: any[] = []; let a: any = null; @@ -2165,7 +2165,7 @@ test.skip("calling render in destroy", async () => { // this nextTick is critical, otherwise jest may silently swallow errors await nextTick(); - expect(steps).toBe(["B:mounted", "B:willUnmount", "B:mounted"]); + expect(steps).toStrictEqual(["B:mounted", "B:willUnmount", "B:mounted"]); expect(fixture.innerHTML).toBe("
A
"); }); @@ -2302,7 +2302,7 @@ test("render method wait until rendering is done", async () => { test("two renderings initiated between willPatch and patched", async () => { let parent: any = null; - let steps: string[] = []; + const steps: string[] = []; class Panel extends Component { static template = xml``; @@ -2325,17 +2325,8 @@ test("two renderings initiated between willPatch and patched", async () => { } await mount(Parent, fixture); - expect(fixture.innerHTML).toBe("
Panel1
"); - parent.state.panel = "Panel2"; - await nextTick(); - expect(fixture.innerHTML).toBe("
Panel2
"); - - parent.state.flag = false; - await nextTick(); - expect(fixture.innerHTML).toBe("
"); - expect(steps).toEqual([ "Parent:setup", "Parent:willStart", @@ -2345,13 +2336,31 @@ test("two renderings initiated between willPatch and patched", async () => { "Panel:render", "Panel:mounted", "Parent:mounted", + ]); + steps.length = 0; + + parent.state.panel = "Panel2"; + await nextTick(); + expect(fixture.innerHTML).toBe("
Panel2
"); + + expect(steps).toEqual([ "Parent:render", - "Panel:willUpdateProps", + "Panel:setup", + "Panel:willStart", "Panel:render", "Parent:willPatch", - "Panel:willPatch", - "Panel:patched", + "Panel:willUnmount", + "Panel:destroyed", + "Panel:mounted", "Parent:patched", + ]); + steps.length = 0; + + parent.state.flag = false; + await nextTick(); + expect(fixture.innerHTML).toBe("
"); + + expect(steps).toEqual([ "Parent:render", "Parent:willPatch", "Panel:willUnmount", diff --git a/tests/components/error_handling.test.ts b/tests/components/error_handling.test.ts index 963f5676..67ef11ef 100644 --- a/tests/components/error_handling.test.ts +++ b/tests/components/error_handling.test.ts @@ -43,8 +43,7 @@ describe("basics", () => { expect(fixture.innerHTML).toBe(""); expect(status(parent)).toBe("destroyed"); expect(error).toBeDefined(); - const regexp = - /Cannot read properties of undefined \(reading 'this'\)|Cannot read property 'this' of undefined/g; + const regexp = /Cannot read properties of undefined \(reading 'this'\)|Cannot read property 'this' of undefined/g; expect(error.message).toMatch(regexp); }); @@ -113,8 +112,7 @@ describe.skip("errors and promises", () => { error = e; } expect(error).toBeDefined(); - const regexp = - /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; + const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error.message).toMatch(regexp); expect(console.error).toBeCalledTimes(0); @@ -227,8 +225,7 @@ describe.skip("errors and promises", () => { error = e; } expect(error).toBeDefined(); - const regexp = - /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; + const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error.message).toMatch(regexp); expect(console.error).toBeCalledTimes(0); @@ -254,8 +251,7 @@ describe.skip("errors and promises", () => { error = e; } expect(error).toBeDefined(); - const regexp = - /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; + const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error.message).toMatch(regexp); expect(console.error).toBeCalledTimes(0); @@ -278,8 +274,7 @@ describe.skip("errors and promises", () => { error = e; } expect(error).toBeDefined(); - const regexp = - /Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g; + const regexp = /Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g; expect(error.message).toMatch(regexp); }); diff --git a/tests/components/style_class.test.ts b/tests/components/style_class.test.ts index 2f75798f..e44a1262 100644 --- a/tests/components/style_class.test.ts +++ b/tests/components/style_class.test.ts @@ -341,8 +341,7 @@ describe("style and class handling", () => { error = e; } expect(error).toBeDefined(); - const regexp = - /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; + const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error.message).toMatch(regexp); expect(fixture.innerHTML).toBe(""); }); diff --git a/tests/components/t_foreach.test.ts b/tests/components/t_foreach.test.ts index aa722108..2a76a885 100644 --- a/tests/components/t_foreach.test.ts +++ b/tests/components/t_foreach.test.ts @@ -260,4 +260,32 @@ describe("list of components", () => { 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((parent.el as HTMLElement).innerHTML).toBe("
1
2
"); + parent.clist = [2, 1]; + await parent.render(); + expect((parent.el as HTMLElement).innerHTML).toBe("
2
1
"); + expect(childInstances.length).toBe(2); + }); }); diff --git a/tests/components/t_key.test.ts b/tests/components/t_key.test.ts new file mode 100644 index 00000000..8fb69f45 --- /dev/null +++ b/tests/components/t_key.test.ts @@ -0,0 +1,208 @@ +import { snapshotEverything, makeTestFixture, addTemplate } from "../helpers"; +import { Component, mount, xml } from "../../src/index"; + +snapshotEverything(); + +let fixture: HTMLElement; + +beforeEach(() => { + fixture = makeTestFixture(); +}); + +describe("t-key", () => { + test("t-key on Component", async () => { + let childInstance = null; + class Child extends Component { + static template = xml`
`; + + setup() { + childInstance = this; + } + } + + class Parent extends Component { + static components = { Child }; + static template = xml``; + + key = 1; + } + + const parent = await mount(Parent, fixture); + expect((parent.el as HTMLElement).innerHTML).toBe("
1
"); + + const oldChild = childInstance; + parent.key = 2; + await parent.render(); + expect((parent.el as HTMLElement).innerHTML).toBe("
2
"); + expect(oldChild === childInstance).toBeFalsy(); + }); + + test("t-key on Component as a function", async () => { + let childInstance = null; + class Child extends Component { + static template = xml`
`; + + setup() { + childInstance = this; + } + } + + let keyCalls = 0; + let __key = 1; + class Parent extends Component { + static components = { Child }; + static template = xml``; + + get key() { + keyCalls++; + return __key; + } + } + + const parent = await mount(Parent, fixture); + expect((parent.el as HTMLElement).innerHTML).toBe("
1
"); + expect(keyCalls).toBe(2); // one for t-key, the other for the props + + const oldChild = childInstance; + __key = 2; + await parent.render(); + expect((parent.el as HTMLElement).innerHTML).toBe("
2
"); + expect(oldChild === childInstance).toBeFalsy(); + expect(keyCalls).toBe(4); + }); + + test("t-key on multiple Components", async () => { + const childInstances = []; + class Child extends Component { + static template = xml`
`; + + setup() { + childInstances.push(this); + } + } + + class Parent extends Component { + static components = { Child }; + static template = xml` + + + `; + + key1 = 1; + key2 = 2; + } + + const parent = await mount(Parent, fixture); + expect((parent.el as HTMLElement).innerHTML).toBe("
1
2
"); + + parent.key1 = 2; + parent.key2 = 1; + await parent.render(); + expect((parent.el as HTMLElement).innerHTML).toBe("
2
1
"); + expect(childInstances.length).toBe(4); + }); + + test("t-key on multiple Components with t-call 1", async () => { + const childInstances = []; + class Child extends Component { + static template = xml`
`; + + setup() { + childInstances.push(this); + } + } + + addTemplate("calledTemplate", ``); + + class Parent extends Component { + static components = { Child }; + static template = xml` + + + `; + + key1 = 1; + key2 = 2; + } + + const parent = await mount(Parent, fixture); + expect((parent.el as HTMLElement).innerHTML).toBe("
1
2
"); + + parent.key1 = 2; + parent.key2 = 1; + await parent.render(); + expect((parent.el as HTMLElement).innerHTML).toBe("
2
1
"); + expect(childInstances.length).toBe(4); + }); + + test("t-key on multiple Components with t-call 2", async () => { + const childInstances = []; + class Child extends Component { + static template = xml`
`; + + setup() { + childInstances.push(this); + } + } + + addTemplate( + "calledTemplate", + `` + ); + + class Parent extends Component { + static components = { Child }; + static template = xml` + + `; + + key1 = 1; + key2 = 2; + } + + const parent = await mount(Parent, fixture); + expect((parent.el as HTMLElement).innerHTML).toBe("
1
2
"); + + parent.key1 = 2; + parent.key2 = 1; + await parent.render(); + expect((parent.el as HTMLElement).innerHTML).toBe("
2
1
"); + expect(childInstances.length).toBe(4); + }); + + test("t-foreach with t-key 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` + + + + `; + + key1 = "key1"; + clist = [1, 2]; + } + + const parent = await mount(Parent, fixture); + expect((parent.el as HTMLElement).innerHTML).toBe("
1key1
2key1
"); + parent.clist = [2, 1]; + await parent.render(); + expect((parent.el as HTMLElement).innerHTML).toBe("
2key1
1key1
"); + expect(childInstances.length).toBe(2); + childInstances.length = 0; + + parent.clist = [1, 2]; + parent.key1 = "key2"; + await parent.render(); + expect((parent.el as HTMLElement).innerHTML).toBe("
1key2
2key2
"); + expect(childInstances.length).toBe(2); + }); +}); diff --git a/tests/misc/portal.test.ts b/tests/misc/portal.test.ts index 2ee53eba..6e7bee56 100644 --- a/tests/misc/portal.test.ts +++ b/tests/misc/portal.test.ts @@ -405,8 +405,7 @@ describe("Portal", () => { error = e; } expect(error).toBeDefined(); - const regexp = - /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; + const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error.message).toMatch(regexp); }); diff --git a/tests/qweb/__snapshots__/t_foreach.test.ts.snap b/tests/qweb/__snapshots__/t_foreach.test.ts.snap index 2b9356fc..de42c63f 100644 --- a/tests/qweb/__snapshots__/t_foreach.test.ts.snap +++ b/tests/qweb/__snapshots__/t_foreach.test.ts.snap @@ -479,7 +479,8 @@ exports[`t-foreach throws error if invalid loop expression 1`] = ` ctx[\`item\`] = v_block2[i1]; ctx[\`item_index\`] = i1; let key1 = ctx['item']; - c_block2[i1] = withKey(block3(), key1); + const tKey_1 = ctx['item_index']; + c_block2[i1] = withKey(block3(), tKey_1 + key1); } let b2 = list(c_block2); return block1([], [b2]); diff --git a/tests/qweb/__snapshots__/t_key.test.ts.snap b/tests/qweb/__snapshots__/t_key.test.ts.snap index a51dfa98..578a716b 100644 --- a/tests/qweb/__snapshots__/t_key.test.ts.snap +++ b/tests/qweb/__snapshots__/t_key.test.ts.snap @@ -9,8 +9,41 @@ exports[`t-key can use t-key directive on a node 1`] = ` let block1 = createBlock(\`
\`); return function template(ctx, node, key = \\"\\") { + const tKey_1 = ctx['beer'].id; let d1 = ctx['beer'].name; - return block1([d1]); + return toggler(tKey_1, block1([d1])); + } +}" +`; + +exports[`t-key can use t-key directive on a node 2 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const tKey_1 = ctx['beer'].id; + let d1 = ctx['beer'].name; + return toggler(tKey_1, block1([d1])); + } +}" +`; + +exports[`t-key can use t-key directive on a node as a function 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const tKey_1 = ctx['getKey'](ctx['beer']); + let d1 = ctx['beer'].name; + return toggler(tKey_1, block1([d1])); } }" `; diff --git a/tests/qweb/__snapshots__/t_ref.test.ts.snap b/tests/qweb/__snapshots__/t_ref.test.ts.snap index d03bbd62..e26efe58 100644 --- a/tests/qweb/__snapshots__/t_ref.test.ts.snap +++ b/tests/qweb/__snapshots__/t_ref.test.ts.snap @@ -102,10 +102,11 @@ exports[`t-ref refs in a loop 1`] = ` for (let i1 = 0; i1 < l_block2; i1++) { ctx[\`item\`] = v_block2[i1]; let key1 = ctx['item']; - const v1 = ctx['item']; - let d1 = (el) => refs[\`\${v1}\`] = el; + const tKey_1 = ctx['item']; + const v2 = ctx['item']; + let d1 = (el) => refs[\`\${v2}\`] = el; let d2 = ctx['item']; - c_block2[i1] = withKey(block3([d1, d2]), key1); + c_block2[i1] = withKey(block3([d1, d2]), tKey_1 + key1); } let b2 = list(c_block2); return block1([], [b2]); diff --git a/tests/qweb/t_key.test.ts b/tests/qweb/t_key.test.ts index 08ec93b5..20ad0643 100644 --- a/tests/qweb/t_key.test.ts +++ b/tests/qweb/t_key.test.ts @@ -1,4 +1,5 @@ -import { renderToString, snapshotEverything } from "../helpers"; +import { renderToString, renderToBdom, snapshotEverything, makeTestFixture } from "../helpers"; +import { mount, patch } from "../../src/blockdom/index"; snapshotEverything(); @@ -10,6 +11,28 @@ describe("t-key", () => { ); }); + test("can use t-key directive on a node as a function", () => { + const template = `
`; + const getKey = (arg: any) => arg.id; + expect(renderToString(template, { getKey, beer: { id: 12, name: "Chimay Rouge" } })).toBe( + "
Chimay Rouge
" + ); + }); + + test("can use t-key directive on a node 2", async () => { + const template = `
`; + const bd = renderToBdom(template, { beer: { id: 12, name: "Chimay Rouge" } }); + const fixture = makeTestFixture(); + await mount(bd, fixture); + const div = fixture.firstChild; + + expect((div as HTMLElement).outerHTML).toBe("
Chimay Rouge
"); + const bd2 = renderToBdom(template, { beer: { id: 13, name: "Chimay Rouge" } }); + await patch(bd, bd2); + expect(div !== fixture.firstChild).toBeTruthy(); + expect((div as HTMLElement).outerHTML).toBe("
Chimay Rouge
"); + }); + test("t-key directive in a list", () => { const template = `