[FIX] component: scope issue with slots

Slot templates need to be able to access variables from the parent
scope.

closes #228
This commit is contained in:
Géry Debongnie
2019-07-05 09:45:55 +02:00
parent be2fc965b1
commit 8e1aa71436
6 changed files with 343 additions and 47 deletions
+14 -10
View File
@@ -274,7 +274,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
}
async render(force: boolean = false, patchQueue?: any[]): Promise<void> {
async render(force: boolean = false, patchQueue?: any[], scope?: any, vars?: any): Promise<void> {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
return;
@@ -283,7 +283,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (shouldPatch) {
patchQueue = [];
}
const renderVDom = this.__render(force, patchQueue);
const renderVDom = this.__render(force, patchQueue, scope, vars);
const renderId = __owl__.renderId;
await renderVDom;
@@ -417,7 +417,9 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
async __updateProps(
nextProps: Props,
forceUpdate: boolean = false,
patchQueue?: any[]
patchQueue?: any[],
scope?: any,
vars?: any,
): Promise<void> {
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
if (shouldUpdate) {
@@ -427,7 +429,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
await this.willUpdateProps(nextProps);
this.props = nextProps;
await this.render(forceUpdate, patchQueue);
await this.render(forceUpdate, patchQueue, scope, vars);
}
}
@@ -441,14 +443,14 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
__owl__.vnode = patch(target, vnode);
}
__prepare(): Promise<VNode> {
__prepare(scope?: Object, vars?: any): Promise<VNode> {
const __owl__ = this.__owl__;
__owl__.renderProps = this.props;
__owl__.renderPromise = this.__prepareAndRender();
__owl__.renderPromise = this.__prepareAndRender(scope, vars);
return __owl__.renderPromise;
}
async __prepareAndRender(): Promise<VNode> {
async __prepareAndRender(scope?: Object, vars?: any): Promise<VNode> {
await this.willStart();
const __owl__ = this.__owl__;
if (__owl__.isDestroyed) {
@@ -480,10 +482,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
__owl__.render = qweb.render.bind(qweb, this.template);
this.__observeState();
return this.__render();
return this.__render(false, [], scope, vars);
}
async __render(force: boolean = false, patchQueue: any[] = []): Promise<VNode> {
async __render(force: boolean = false, patchQueue: any[] = [], scope?: Object, vars?: any): Promise<VNode> {
const __owl__ = this.__owl__;
__owl__.renderId++;
const promises: Promise<void>[] = [];
@@ -499,7 +501,9 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
handlers: __owl__.boundHandlers,
mountedHandlers: __owl__.mountedHandlers,
forceUpdate: force,
patchQueue
patchQueue,
scope,
vars
});
patch.push(vnode);
if (__owl__.observer) {
+59 -24
View File
@@ -288,45 +288,44 @@ export class QWeb extends EventBus {
return template.fn.call(this, context, extra);
}
_compile(name: string, elem: Element, parentNode?: number): CompiledTemplate {
_compile(name: string, elem: Element, parentContext?: Context): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new Context(name);
if (parentNode) {
ctx.nextID = parentNode + 1;
ctx.parentNode = parentNode;
if (parentContext) {
ctx.variables = Object.create(parentContext.variables);
ctx.nextID = parentContext.parentNode! + 1;
ctx.parentNode = parentContext.parentNode!;
ctx.allowMultipleRoots = true;
ctx.addLine(`let c${parentNode} = extra.parentNode;`);
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
for (let v in parentContext.variables) {
let variable = <any>parentContext.variables[v];
if (variable.id) {
ctx.addLine(`let ${variable.id} = extra.vars.${variable.id}`);
}
}
}
if (parentContext) {
ctx.addLine(" Object.assign(context, extra.scope);");
}
this._compileNode(elem, ctx);
if (ctx.shouldProtectContext) {
ctx.code.unshift(" context = Object.create(context);");
}
if (ctx.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
ctx.code.unshift(" let owner = context;");
}
if (ctx.shouldDefineQWeb) {
ctx.code.unshift(" let QWeb = this.constructor;");
}
if (ctx.shouldDefineUtils) {
ctx.code.unshift(" let utils = this.utils;");
}
if (!parentNode) {
if (!parentContext) {
if (!ctx.rootNode) {
throw new Error("A template should have one root node");
}
ctx.addLine(`return vn${ctx.rootNode};`);
}
let code = ctx.generateCode();
let template;
try {
template = new Function("context", "extra", ctx.code.join("\n")) as CompiledTemplate;
template = new Function("context", "extra", code.join("\n")) as CompiledTemplate;
} catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(ctx.code.join("\n"));
console.warn(code.join("\n"));
console.groupEnd();
throw new Error(
`Invalid generated code while compiling template '${templateName}': ${e.message}`
@@ -672,10 +671,12 @@ export class Context {
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
inLoop: boolean = false;
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
scopeVars: any[] = [];
constructor(name?: string) {
this.rootContext = this;
@@ -688,6 +689,34 @@ export class Context {
return id;
}
generateCode(): string[] {
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
if (shouldTrackScope) {
// add some vars to scope if needed
for (let scopeVar of this.scopeVars.reverse()) {
let { index, key, indent } = scopeVar;
const prefix = new Array(indent + 2).join(" ");
this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
}
this.code.unshift(" const scope = Object.create(null);");
}
if (this.shouldProtectContext) {
this.code.unshift(" context = Object.create(context);");
}
if (this.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
this.code.unshift(" let owner = context;");
}
if (this.shouldDefineQWeb) {
this.code.unshift(" let QWeb = this.constructor;");
}
if (this.shouldDefineUtils) {
this.code.unshift(" let utils = this.utils;");
}
return this.code;
}
withParent(node: number): Context {
if (
!this.allowMultipleRoots &&
@@ -716,9 +745,15 @@ export class Context {
this.indentLevel--;
}
addLine(line: string) {
addLine(line: string): number {
const prefix = new Array(this.indentLevel + 2).join(" ");
this.code.push(prefix + line);
return this.code.length - 1;
}
addToScope(key: string, expr: string) {
const index = this.addLine(`context.${key} = ${expr};`);
this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel });
}
addIf(condition: string) {
+5 -5
View File
@@ -255,11 +255,11 @@ QWeb.addDirective({
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
ctx.indent();
ctx.addLine(`context.${name}_first = i === 0;`);
ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`);
ctx.addLine(`context.${name}_index = i;`);
ctx.addLine(`context.${name} = _${keysID}[i];`);
ctx.addLine(`context.${name}_value = _${valuesID}[i];`);
ctx.addToScope(name + '_first', 'i === 0');
ctx.addToScope(name + '_last', `i === _length${keysID} - 1`);
ctx.addToScope(name + '_index', 'i');
ctx.addToScope(name, `_${keysID}[i]`);
ctx.addToScope(name + '_value', `_${valuesID}[i]`);
const nodeCopy = <Element>node.cloneNode(true);
let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
if (!shouldWarn && node.tagName === "t") {
+18 -6
View File
@@ -556,7 +556,16 @@ QWeb.addDirective({
ctx.addLine(`context.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`);
// SLOTS
if (node.childNodes.length) {
const varDefs: string[] = [];
const hasSlots = node.childNodes.length;
if (hasSlots) {
ctx.rootContext.shouldTrackScope = true;
for (let v of Object.values(ctx.variables)) {
if (v["id"]) {
varDefs.push(v["id"]);
}
}
const clone = <Element>node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]");
const slotId = qweb.nextSlotId++;
@@ -567,7 +576,7 @@ QWeb.addDirective({
slotNode.parentElement!.removeChild(slotNode);
const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set");
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx.parentNode!);
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
}
}
@@ -576,12 +585,14 @@ QWeb.addDirective({
for (let child of Object.values(clone.childNodes)) {
t.appendChild(child);
}
const slotFn = qweb._compile(`slot_default_template`, t, ctx.parentNode!);
const slotFn = qweb._compile(`slot_default_template`, t, ctx);
qweb.slots[`${slotId}_default`] = slotFn.bind(qweb);
}
}
ctx.addLine(`def${defID} = w${componentID}.__prepare();`);
const scopeVars =
hasSlots && ctx.scopeVars.length ? `Object.assign({}, scope), {${varDefs.join(",")}}` : "";
ctx.addLine(`def${defID} = w${componentID}.__prepare(${scopeVars});`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
ctx.addLine(
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});c${
@@ -596,7 +607,8 @@ QWeb.addDirective({
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
}
ctx.addLine(
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode});`
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode}${scopeVars &&
", " + scopeVars});`
);
let keepAliveCode = "";
if (keepAlive) {
@@ -774,7 +786,7 @@ QWeb.addDirective({
ctx.addLine(
`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${
ctx.parentNode
}}));`
}, vars: extra.vars}));`
);
ctx.closeIf();
return true;