mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[FIX] prevent crash in case with t-foreach and t-out with components
The t-out directive is compiled internally into a LazyValue, which represents a value that may or may not be created sometimes in the future. It can also be reused more than once, and this is where there may be an issue: if a component is contained in the lazyvalue, it needs a unique key (coming from the t-foreach) to be properly indexed in the parent children map. However, the LazyValue does not keep the key information, so it is not able to provide it to its content. The fix is then quite clear: the LazyValue class should store the key information, and provides it to its content. This allows the LazyValue to be used multiple times, in any place in a template. closes #1270
This commit is contained in:
committed by
Sam Degueldre
parent
669fd622ec
commit
7ab34c5ca5
@@ -214,6 +214,14 @@ class CodeTarget {
|
|||||||
result.push(`}`);
|
result.push(`}`);
|
||||||
return result.join("\n ");
|
return result.join("\n ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentKey(ctx: Context) {
|
||||||
|
let key = this.loopLevel ? `key${this.loopLevel}` : "key";
|
||||||
|
if (ctx.tKeyExpr) {
|
||||||
|
key = `${ctx.tKeyExpr} + ${key}`;
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
||||||
@@ -365,19 +373,15 @@ export class CodeGenerator {
|
|||||||
|
|
||||||
insertBlock(expression: string, block: BlockDescription, ctx: Context): void {
|
insertBlock(expression: string, block: BlockDescription, ctx: Context): void {
|
||||||
let blockExpr = block.generateExpr(expression);
|
let blockExpr = block.generateExpr(expression);
|
||||||
const tKeyExpr = ctx.tKeyExpr;
|
|
||||||
if (block.parentVar) {
|
if (block.parentVar) {
|
||||||
let keyArg = `key${this.target.loopLevel}`;
|
let key = this.target.currentKey(ctx);
|
||||||
if (tKeyExpr) {
|
|
||||||
keyArg = `${tKeyExpr} + ${keyArg}`;
|
|
||||||
}
|
|
||||||
this.helpers.add("withKey");
|
this.helpers.add("withKey");
|
||||||
this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${keyArg});`);
|
this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${key});`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tKeyExpr) {
|
if (ctx.tKeyExpr) {
|
||||||
blockExpr = `toggler(${tKeyExpr}, ${blockExpr})`;
|
blockExpr = `toggler(${ctx.tKeyExpr}, ${blockExpr})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block.isRoot && !ctx.preventRoot) {
|
if (block.isRoot && !ctx.preventRoot) {
|
||||||
@@ -1053,7 +1057,8 @@ export class CodeGenerator {
|
|||||||
this.helpers.add("LazyValue");
|
this.helpers.add("LazyValue");
|
||||||
const bodyAst: AST = { type: ASTType.Multi, content: ast.body };
|
const bodyAst: AST = { type: ASTType.Multi, content: ast.body };
|
||||||
const name = this.compileInNewTarget("value", bodyAst, ctx);
|
const name = this.compileInNewTarget("value", bodyAst, ctx);
|
||||||
let value = `new LazyValue(${name}, ctx, this, node)`;
|
let key = this.target.currentKey(ctx);
|
||||||
|
let value = `new LazyValue(${name}, ctx, this, node, ${key})`;
|
||||||
value = ast.value ? (value ? `withDefault(${expr}, ${value})` : expr) : value;
|
value = ast.value ? (value ? `withDefault(${expr}, ${value})` : expr) : value;
|
||||||
this.addLine(`ctx[\`${ast.name}\`] = ${value};`);
|
this.addLine(`ctx[\`${ast.name}\`] = ${value};`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -111,15 +111,18 @@ class LazyValue {
|
|||||||
ctx: any;
|
ctx: any;
|
||||||
component: any;
|
component: any;
|
||||||
node: any;
|
node: any;
|
||||||
constructor(fn: any, ctx: any, component: any, node: any) {
|
key: any;
|
||||||
|
|
||||||
|
constructor(fn: any, ctx: any, component: any, node: any, key: any) {
|
||||||
this.fn = fn;
|
this.fn = fn;
|
||||||
this.ctx = capture(ctx);
|
this.ctx = capture(ctx);
|
||||||
this.component = component;
|
this.component = component;
|
||||||
this.node = node;
|
this.node = node;
|
||||||
|
this.key = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluate(): any {
|
evaluate(): any {
|
||||||
return this.fn.call(this.component, this.ctx, this.node);
|
return this.fn.call(this.component, this.ctx, this.node, this.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ exports[`t-esc t-esc is escaped 1`] = `
|
|||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
ctx = Object.create(ctx);
|
ctx = Object.create(ctx);
|
||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
ctx[\`var\`] = new LazyValue(value1, ctx, this, node);
|
ctx[\`var\`] = new LazyValue(value1, ctx, this, node, key);
|
||||||
let txt1 = ctx['var'];
|
let txt1 = ctx['var'];
|
||||||
return block1([txt1]);
|
return block1([txt1]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ exports[`t-out t-out bdom 1`] = `
|
|||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
ctx = Object.create(ctx);
|
ctx = Object.create(ctx);
|
||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
ctx[\`var\`] = new LazyValue(value1, ctx, this, node);
|
ctx[\`var\`] = new LazyValue(value1, ctx, this, node, key);
|
||||||
const b3 = safeOutput(ctx['var']);
|
const b3 = safeOutput(ctx['var']);
|
||||||
return block1([], [b3]);
|
return block1([], [b3]);
|
||||||
}
|
}
|
||||||
@@ -310,7 +310,7 @@ exports[`t-out t-out switch markup on bdom 1`] = `
|
|||||||
ctx = Object.create(ctx);
|
ctx = Object.create(ctx);
|
||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
let b3,b5;
|
let b3,b5;
|
||||||
ctx[\`bdom\`] = new LazyValue(value1, ctx, this, node);
|
ctx[\`bdom\`] = new LazyValue(value1, ctx, this, node, key);
|
||||||
if (ctx['hasBdom']) {
|
if (ctx['hasBdom']) {
|
||||||
const b4 = safeOutput(ctx['bdom']);
|
const b4 = safeOutput(ctx['bdom']);
|
||||||
b3 = block3([], [b4]);
|
b3 = block3([], [b4]);
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ exports[`t-set set from body literal (with t-if/t-else 1`] = `
|
|||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
ctx = Object.create(ctx);
|
ctx = Object.create(ctx);
|
||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
ctx[\`value\`] = new LazyValue(value1, ctx, this, node);
|
ctx[\`value\`] = new LazyValue(value1, ctx, this, node, key);
|
||||||
return text(ctx['value']);
|
return text(ctx['value']);
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@@ -142,7 +142,7 @@ exports[`t-set set from body lookup 1`] = `
|
|||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
ctx = Object.create(ctx);
|
ctx = Object.create(ctx);
|
||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
ctx[\`stuff\`] = new LazyValue(value1, ctx, this, node);
|
ctx[\`stuff\`] = new LazyValue(value1, ctx, this, node, key);
|
||||||
let txt1 = ctx['stuff'];
|
let txt1 = ctx['stuff'];
|
||||||
return block1([txt1]);
|
return block1([txt1]);
|
||||||
}
|
}
|
||||||
@@ -206,7 +206,7 @@ exports[`t-set t-set body is evaluated immediately 1`] = `
|
|||||||
ctx = Object.create(ctx);
|
ctx = Object.create(ctx);
|
||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
setContextValue(ctx, \\"v1\\", 'before');
|
setContextValue(ctx, \\"v1\\", 'before');
|
||||||
ctx[\`v2\`] = new LazyValue(value1, ctx, this, node);
|
ctx[\`v2\`] = new LazyValue(value1, ctx, this, node, key);
|
||||||
setContextValue(ctx, \\"v1\\", 'after');
|
setContextValue(ctx, \\"v1\\", 'after');
|
||||||
const b3 = safeOutput(ctx['v2']);
|
const b3 = safeOutput(ctx['v2']);
|
||||||
return block1([], [b3]);
|
return block1([], [b3]);
|
||||||
@@ -471,7 +471,7 @@ exports[`t-set t-set with content and sub t-esc 1`] = `
|
|||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
ctx = Object.create(ctx);
|
ctx = Object.create(ctx);
|
||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
ctx[\`setvar\`] = new LazyValue(value1, ctx, this, node);
|
ctx[\`setvar\`] = new LazyValue(value1, ctx, this, node, key);
|
||||||
let txt1 = ctx['setvar'];
|
let txt1 = ctx['setvar'];
|
||||||
return block1([txt1]);
|
return block1([txt1]);
|
||||||
}
|
}
|
||||||
@@ -497,7 +497,7 @@ exports[`t-set t-set with t-value (falsy) and body 1`] = `
|
|||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
setContextValue(ctx, \\"v3\\", false);
|
setContextValue(ctx, \\"v3\\", false);
|
||||||
setContextValue(ctx, \\"v1\\", 'before');
|
setContextValue(ctx, \\"v1\\", 'before');
|
||||||
ctx[\`v2\`] = withDefault(ctx['v3'], new LazyValue(value1, ctx, this, node));
|
ctx[\`v2\`] = withDefault(ctx['v3'], new LazyValue(value1, ctx, this, node, key));
|
||||||
setContextValue(ctx, \\"v1\\", 'after');
|
setContextValue(ctx, \\"v1\\", 'after');
|
||||||
setContextValue(ctx, \\"v3\\", true);
|
setContextValue(ctx, \\"v3\\", true);
|
||||||
const b3 = safeOutput(ctx['v2']);
|
const b3 = safeOutput(ctx['v2']);
|
||||||
@@ -525,7 +525,7 @@ exports[`t-set t-set with t-value (truthy) and body 1`] = `
|
|||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
setContextValue(ctx, \\"v3\\", 'Truthy');
|
setContextValue(ctx, \\"v3\\", 'Truthy');
|
||||||
setContextValue(ctx, \\"v1\\", 'before');
|
setContextValue(ctx, \\"v1\\", 'before');
|
||||||
ctx[\`v2\`] = withDefault(ctx['v3'], new LazyValue(value1, ctx, this, node));
|
ctx[\`v2\`] = withDefault(ctx['v3'], new LazyValue(value1, ctx, this, node, key));
|
||||||
setContextValue(ctx, \\"v1\\", 'after');
|
setContextValue(ctx, \\"v1\\", 'after');
|
||||||
setContextValue(ctx, \\"v3\\", false);
|
setContextValue(ctx, \\"v3\\", false);
|
||||||
const b3 = safeOutput(ctx['v2']);
|
const b3 = safeOutput(ctx['v2']);
|
||||||
@@ -638,7 +638,7 @@ exports[`t-set value priority (with non text body 1`] = `
|
|||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
ctx = Object.create(ctx);
|
ctx = Object.create(ctx);
|
||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
ctx[\`value\`] = withDefault(1, new LazyValue(value1, ctx, this, node));
|
ctx[\`value\`] = withDefault(1, new LazyValue(value1, ctx, this, node, key));
|
||||||
let txt1 = ctx['value'];
|
let txt1 = ctx['value'];
|
||||||
return block1([txt1]);
|
return block1([txt1]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ exports[`basics t-set with a body expression can be passed in props, and then t-
|
|||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
ctx = Object.create(ctx);
|
ctx = Object.create(ctx);
|
||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
ctx[\`abc\`] = new LazyValue(value1, ctx, this, node);
|
ctx[\`abc\`] = new LazyValue(value1, ctx, this, node, key);
|
||||||
const b3 = comp1({val: ctx['abc']}, key + \`__1\`, node, this, null);
|
const b3 = comp1({val: ctx['abc']}, key + \`__1\`, node, this, null);
|
||||||
return block1([], [b3]);
|
return block1([], [b3]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`components in t-out simple list 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { prepareList, isBoundary, withDefault, LazyValue, safeOutput, withKey } = helpers;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, true);
|
||||||
|
|
||||||
|
function value1(ctx, node, key = \\"\\") {
|
||||||
|
return comp1({}, key + \`__1\`, node, this, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
ctx = Object.create(ctx);
|
||||||
|
ctx[isBoundary] = 1
|
||||||
|
ctx = Object.create(ctx);
|
||||||
|
const [k_block1, v_block1, l_block1, c_block1] = prepareList([1,2]);;
|
||||||
|
for (let i1 = 0; i1 < l_block1; i1++) {
|
||||||
|
ctx[\`n\`] = v_block1[i1];
|
||||||
|
const key1 = ctx['n'];
|
||||||
|
ctx[\`blabla\`] = new LazyValue(value1, ctx, this, node, key1);
|
||||||
|
c_block1[i1] = withKey(safeOutput(ctx['blabla']), key1);
|
||||||
|
}
|
||||||
|
return list(c_block1);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components in t-out simple list 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`child\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
@@ -59,7 +59,7 @@ exports[`t-set slots with a t-set with a component in body 1`] = `
|
|||||||
function slot1(ctx, node, key = \\"\\") {
|
function slot1(ctx, node, key = \\"\\") {
|
||||||
ctx = Object.create(ctx);
|
ctx = Object.create(ctx);
|
||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
ctx[\`v\`] = new LazyValue(value1, ctx, this, node);
|
ctx[\`v\`] = new LazyValue(value1, ctx, this, node, key);
|
||||||
const b3 = text(\` in slot \`);
|
const b3 = text(\` in slot \`);
|
||||||
const b4 = safeOutput(ctx['v']);
|
const b4 = safeOutput(ctx['v']);
|
||||||
return multi([b3, b4]);
|
return multi([b3, b4]);
|
||||||
@@ -114,7 +114,7 @@ exports[`t-set slots with an t-set with a component in body 1`] = `
|
|||||||
function slot1(ctx, node, key = \\"\\") {
|
function slot1(ctx, node, key = \\"\\") {
|
||||||
ctx = Object.create(ctx);
|
ctx = Object.create(ctx);
|
||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
ctx[\`v\`] = new LazyValue(value1, ctx, this, node);
|
ctx[\`v\`] = new LazyValue(value1, ctx, this, node, key);
|
||||||
const b5 = text(\` tea \`);
|
const b5 = text(\` tea \`);
|
||||||
const b6 = safeOutput(ctx['v']);
|
const b6 = safeOutput(ctx['v']);
|
||||||
return multi([b5, b6]);
|
return multi([b5, b6]);
|
||||||
@@ -169,7 +169,7 @@ exports[`t-set slots with an unused t-set with a component in body 1`] = `
|
|||||||
function slot1(ctx, node, key = \\"\\") {
|
function slot1(ctx, node, key = \\"\\") {
|
||||||
ctx = Object.create(ctx);
|
ctx = Object.create(ctx);
|
||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
ctx[\`v\`] = new LazyValue(value1, ctx, this, node);
|
ctx[\`v\`] = new LazyValue(value1, ctx, this, node, key);
|
||||||
return text(\` in slot \`);
|
return text(\` in slot \`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,7 +343,7 @@ exports[`t-set t-set with a component in body 1`] = `
|
|||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
ctx = Object.create(ctx);
|
ctx = Object.create(ctx);
|
||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
ctx[\`v\`] = new LazyValue(value1, ctx, this, node);
|
ctx[\`v\`] = new LazyValue(value1, ctx, this, node, key);
|
||||||
const b3 = safeOutput(ctx['v']);
|
const b3 = safeOutput(ctx['v']);
|
||||||
return block1([], [b3]);
|
return block1([], [b3]);
|
||||||
}
|
}
|
||||||
@@ -377,7 +377,7 @@ exports[`t-set t-set with something in body 1`] = `
|
|||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
ctx = Object.create(ctx);
|
ctx = Object.create(ctx);
|
||||||
ctx[isBoundary] = 1
|
ctx[isBoundary] = 1
|
||||||
ctx[\`v\`] = new LazyValue(value1, ctx, this, node);
|
ctx[\`v\`] = new LazyValue(value1, ctx, this, node, key);
|
||||||
const b3 = safeOutput(ctx['v']);
|
const b3 = safeOutput(ctx['v']);
|
||||||
return block1([], [b3]);
|
return block1([], [b3]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Component, mount, xml } from "../../src/index";
|
||||||
|
import { makeTestFixture, snapshotEverything } from "../helpers";
|
||||||
|
|
||||||
|
snapshotEverything();
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// t-out
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("components in t-out", () => {
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simple list", async () => {
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`child`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<t t-foreach="[1,2]" t-as="n" t-key="n">
|
||||||
|
<t t-set="blabla">
|
||||||
|
<Child />
|
||||||
|
</t>
|
||||||
|
<t t-out="blabla"/>
|
||||||
|
</t>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Parent, fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("childchild");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user