[IMP] qweb: add t-async directive on t-widget

Closes #98
This commit is contained in:
Aaron Bohy
2019-06-14 10:17:13 +02:00
committed by Géry Debongnie
parent aa406b53e2
commit e64e415b3b
8 changed files with 636 additions and 72 deletions
+19 -2
View File
@@ -10,6 +10,7 @@
- [Methods](#methods)
- [Lifecycle](#lifecycle)
- [Composition](#composition)
- [Asynchronous rendering](#asynchronous-rendering)
- [Event Handling](#event-handling)
- [`t-key` directive](#t-key-directive)
- [`t-mounted` directive](#t-mounted-directive)
@@ -309,7 +310,7 @@ the DOM. This is a good place to remove some listeners, for example.
This is the opposite method of `mounted`.
## Composition
### Composition
The example above shows a QWeb template with a `t-on-click` directive. Widget
templates are standard [QWeb](qweb.md) templates, but with an extra directive:
@@ -828,9 +829,25 @@ There are two different common problems with Owl asynchronous rendering model:
Here are a few tips on how to work with asynchronous widgets:
1. minimize the use of asynchronous widgets!
1. Minimize the use of asynchronous widgets!
2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
synchronous renderings
3. Lazy loading external libraries is a good use case for async rendering. This
is mostly fine, because we can assume that it will only takes a fraction of a
second, and only once (see `owl.utils.loadJS`)
4. For all the other cases, the `t-async` directive (to use alongside
`t-widget`) is there to help you. When this directive is met, a new rendering
sub tree is created, such that the rendering of that component (and its
children) is not tied to the rendering of the rest of the interface. It can
be used on an asynchronous component, to prevent it from delaying the
rendering of the whole interface, or on a synchronous one, such that its
rendering isn't delayed by other (asynchronous) components. Note that this
directive has no effect on the first rendering, but only on subsequent ones
(triggered by state or props changes).
```xml
<div t-name="ParentWidget">
<t t-widget="SyncChild"/>
<t t-widget="AsyncChild" t-async="1"/>
</div>
```
+9 -9
View File
@@ -65,15 +65,15 @@ We present here a list of all standard QWeb directives:
The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives:
| Name | Description |
| ------------------------- | --------------------------------------------------------------------------------------- |
| `t-widget`, `t-keepalive` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#keeping-references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
| `t-slot` | [Rendering a slot](component.md#slots) |
| Name | Description |
| ------------------------------------ | --------------------------------------------------------------------------------------- |
| `t-widget`, `t-keepalive`, `t-async` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#keeping-references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
| `t-slot` | [Rendering a slot](component.md#slots) |
## QWeb Engine
+24 -14
View File
@@ -274,20 +274,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
// we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called.
const patchLen = patchQueue!.length;
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue![i];
patch.push(patch[0].willPatch());
}
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue![i];
patch[0]._patch(patch[1]);
}
for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue![i];
patch[0].patched(patch[2]);
}
this._applyPatchQueue(<any[]>patchQueue);
}
}
@@ -568,6 +555,29 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
return <Props>props;
}
/**
* Apply the given patch queue. A patch is a pair [c, vn], where c is a
* Component instance and vn a VNode.
* 1) Call 'willPatch' on the component of each patch
* 2) Call '_patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in inverse order
*/
_applyPatchQueue(patchQueue: any[]) {
const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch.push(patch[0].willPatch());
}
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch[0]._patch(patch[1]);
}
for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i];
patch[0].patched(patch[2]);
}
}
/**
* Validate the component props (or next props) against the (static) props
* description. This is potentially an expensive operation: it may needs to
+25 -13
View File
@@ -225,13 +225,6 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* // t-widget directive: we start by evaluating the expression given by t-key:
* let key5 = "somestring";
*
* // We keep the index of the position of the widget in the closure. We push
* // null to reserve the slot, and will replace it later by the widget vnode,
* // when it will be ready (do not forget that preparing/rendering a widget is
* // asynchronous)
* let _2_index = c1.length;
* c1.push(null);
*
* // def3 is the promise that will contain later either the new widget
* // creation, or the props update...
* let def3;
@@ -246,6 +239,13 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* ? context.__owl__.children[context.__owl__.cmap[key5]]
* : false;
*
* // We keep the index of the position of the widget in the closure. We push
* // null to reserve the slot, and will replace it later by the widget vnode,
* // when it will be ready (do not forget that preparing/rendering a widget is
* // asynchronous)
* let _2_index = c1.length;
* c1.push(null);
*
* // we evaluate here the props given to the component. It is done here to be
* // able to easily reference it later, and also, it might be an expensive
* // computation, so it is certainly better to do it only once
@@ -360,7 +360,7 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
QWeb.addDirective({
name: "widget",
extraNames: ["props", "keepalive"],
extraNames: ["props", "keepalive", "async"],
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//WIDGET");
@@ -368,6 +368,7 @@ QWeb.addDirective({
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
let async = node.getAttribute("t-async") ? true : false;
// t-on- events and t-transition
const events: [string, string[], string, string][] = [];
@@ -413,8 +414,6 @@ QWeb.addDirective({
// want to evaluate it only once)
ctx.addLine(`let key${keyID} = ${key};`);
}
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
ctx.addLine(`c${ctx.parentNode}.push(null);`);
ctx.addLine(`let def${defID};`);
let templateID = key
? `key${keyID}`
@@ -505,6 +504,13 @@ QWeb.addDirective({
ctx.addLine(
`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`
);
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
if (async) {
ctx.addLine(`const patchQueue${widgetID} = [];`);
ctx.addLine(`c${ctx.parentNode}.push(w${widgetID} && w${widgetID}.__owl__.pvnode || null);`);
} else {
ctx.addLine(`c${ctx.parentNode}.push(null);`);
}
ctx.addLine(`let props${widgetID} = {${propStr}};`);
ctx.addIf(
`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode`
@@ -551,7 +557,6 @@ QWeb.addDirective({
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
// FIXME: click to re-add widget during remove transition -> leak
ctx.addLine(
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeWidgetCode}}}});c${
ctx.parentNode
@@ -560,8 +565,9 @@ QWeb.addDirective({
ctx.addElse();
// need to update widget
const patchQueueCode = async ? `patchQueue${widgetID}` : "extra.patchQueue";
ctx.addLine(
`def${defID} = def${defID} || w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`
`def${defID} = def${defID} || w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, ${patchQueueCode});`
);
let keepAliveCode = "";
if (keepAlive) {
@@ -576,7 +582,13 @@ QWeb.addDirective({
);
ctx.closeIf();
ctx.addLine(`extra.promises.push(def${defID});`);
if (async) {
ctx.addLine(
`def${defID}.then(w${widgetID}._applyPatchQueue.bind(w${widgetID}, patchQueue${widgetID}));`
);
} else {
ctx.addLine(`extra.promises.push(def${defID});`);
}
if (
node.hasAttribute("t-if") ||
+4 -4
View File
@@ -10,10 +10,10 @@ exports[`animations t-transition combined with t-widget 1`] = `
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -54,10 +54,10 @@ exports[`animations t-transition combined with t-widget and t-if 1`] = `
var vn1 = h('div', p1, c1);
if (context['state'].display) {
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
+273 -30
View File
@@ -1,5 +1,248 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`async rendering delayed t-widget with t-async directive 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('button', p2, c2);
c1.push(vn2);
if (!context['updateApp']) {
throw new Error('Missing handler \\\\'' + 'updateApp' + \`\\\\' when evaluating template 'Parent'\`)
}
extra.handlers['click' + 2] = extra.handlers['click' + 2] || context['updateApp'].bind(owner);
p2.on['click'] = extra.handlers['click' + 2];
c2.push({text: \`Update App State\`});
var _3 = 'children';
let c4 = [], p4 = {key:4,attrs:{class: _3}};
var vn4 = h('div', p4, c4);
c1.push(vn4);
//WIDGET
let def6;
let w7 = 7 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[7]] : false;
let _5_index = c4.length;
c4.push(null);
let props7 = {val:context['state'].val};
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
def6 = w7.__owl__.renderPromise;
} else {
w7.destroy();
w7 = false;
}
}
if (!w7) {
let widgetKey7 = \`Child\`;
let W7 = context.widgets && context.widgets[widgetKey7] || QWeb.widgets[widgetKey7];
if (!W7) {throw new Error('Cannot find the definition of widget \\"' + widgetKey7 + '\\"')}
w7 = new W7(owner, props7);
context.__owl__.cmap[7] = w7.__owl__.id;
def6 = w7._prepare();
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: 7, hook: {insert(vn) {let nvn=w7._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c4[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
} else {
def6 = def6 || w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c4[_5_index]=pvnode;});
}
extra.promises.push(def6);
//WIDGET
let def9;
let w10 = 10 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[10]] : false;
let _8_index = c4.length;
const patchQueue10 = [];
c4.push(w10 && w10.__owl__.pvnode || null);
let props10 = {val:context['state'].val};
if (w10 && w10.__owl__.renderPromise && !w10.__owl__.vnode) {
if (utils.shallowEqual(props10, w10.__owl__.renderProps)) {
def9 = w10.__owl__.renderPromise;
} else {
w10.destroy();
w10 = false;
}
}
if (!w10) {
let widgetKey10 = \`AsyncChild\`;
let W10 = context.widgets && context.widgets[widgetKey10] || QWeb.widgets[widgetKey10];
if (!W10) {throw new Error('Cannot find the definition of widget \\"' + widgetKey10 + '\\"')}
w10 = new W10(owner, props10);
context.__owl__.cmap[10] = w10.__owl__.id;
def9 = w10._prepare();
def9 = def9.then(vnode=>{let pvnode=h(vnode.sel, {key: 10, hook: {insert(vn) {let nvn=w10._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w10.destroy();}}});c4[_8_index]=pvnode;w10.__owl__.pvnode = pvnode;});
} else {
def9 = def9 || w10._updateProps(props10, extra.forceUpdate, patchQueue10);
def9 = def9.then(()=>{if (w10.__owl__.isDestroyed) {return};let pvnode=w10.__owl__.pvnode;c4[_8_index]=pvnode;});
}
def9.then(w10._applyPatchQueue.bind(w10, patchQueue10));
return vn1;
}"
`;
exports[`async rendering fast t-widget with t-async directive 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('button', p2, c2);
c1.push(vn2);
if (!context['updateApp']) {
throw new Error('Missing handler \\\\'' + 'updateApp' + \`\\\\' when evaluating template 'Parent'\`)
}
extra.handlers['click' + 2] = extra.handlers['click' + 2] || context['updateApp'].bind(owner);
p2.on['click'] = extra.handlers['click' + 2];
c2.push({text: \`Update App State\`});
var _3 = 'children';
let c4 = [], p4 = {key:4,attrs:{class: _3}};
var vn4 = h('div', p4, c4);
c1.push(vn4);
//WIDGET
let def6;
let w7 = 7 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[7]] : false;
let _5_index = c4.length;
const patchQueue7 = [];
c4.push(w7 && w7.__owl__.pvnode || null);
let props7 = {val:context['state'].val};
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
def6 = w7.__owl__.renderPromise;
} else {
w7.destroy();
w7 = false;
}
}
if (!w7) {
let widgetKey7 = \`Child\`;
let W7 = context.widgets && context.widgets[widgetKey7] || QWeb.widgets[widgetKey7];
if (!W7) {throw new Error('Cannot find the definition of widget \\"' + widgetKey7 + '\\"')}
w7 = new W7(owner, props7);
context.__owl__.cmap[7] = w7.__owl__.id;
def6 = w7._prepare();
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: 7, hook: {insert(vn) {let nvn=w7._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c4[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
} else {
def6 = def6 || w7._updateProps(props7, extra.forceUpdate, patchQueue7);
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c4[_5_index]=pvnode;});
}
def6.then(w7._applyPatchQueue.bind(w7, patchQueue7));
//WIDGET
let def9;
let w10 = 10 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[10]] : false;
let _8_index = c4.length;
c4.push(null);
let props10 = {val:context['state'].val};
if (w10 && w10.__owl__.renderPromise && !w10.__owl__.vnode) {
if (utils.shallowEqual(props10, w10.__owl__.renderProps)) {
def9 = w10.__owl__.renderPromise;
} else {
w10.destroy();
w10 = false;
}
}
if (!w10) {
let widgetKey10 = \`AsyncChild\`;
let W10 = context.widgets && context.widgets[widgetKey10] || QWeb.widgets[widgetKey10];
if (!W10) {throw new Error('Cannot find the definition of widget \\"' + widgetKey10 + '\\"')}
w10 = new W10(owner, props10);
context.__owl__.cmap[10] = w10.__owl__.id;
def9 = w10._prepare();
def9 = def9.then(vnode=>{let pvnode=h(vnode.sel, {key: 10, hook: {insert(vn) {let nvn=w10._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w10.destroy();}}});c4[_8_index]=pvnode;w10.__owl__.pvnode = pvnode;});
} else {
def9 = def9 || w10._updateProps(props10, extra.forceUpdate, extra.patchQueue);
def9 = def9.then(()=>{if (w10.__owl__.isDestroyed) {return};let pvnode=w10.__owl__.pvnode;c4[_8_index]=pvnode;});
}
extra.promises.push(def9);
return vn1;
}"
`;
exports[`async rendering t-widget with t-async directive: mixed re-renderings 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('button', p2, c2);
c1.push(vn2);
if (!context['updateApp']) {
throw new Error('Missing handler \\\\'' + 'updateApp' + \`\\\\' when evaluating template 'Parent'\`)
}
extra.handlers['click' + 2] = extra.handlers['click' + 2] || context['updateApp'].bind(owner);
p2.on['click'] = extra.handlers['click' + 2];
c2.push({text: \`Update App State\`});
var _3 = 'children';
let c4 = [], p4 = {key:4,attrs:{class: _3}};
var vn4 = h('div', p4, c4);
c1.push(vn4);
//WIDGET
let def6;
let w7 = 7 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[7]] : false;
let _5_index = c4.length;
c4.push(null);
let props7 = {val:context['state'].val};
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
def6 = w7.__owl__.renderPromise;
} else {
w7.destroy();
w7 = false;
}
}
if (!w7) {
let widgetKey7 = \`Child\`;
let W7 = context.widgets && context.widgets[widgetKey7] || QWeb.widgets[widgetKey7];
if (!W7) {throw new Error('Cannot find the definition of widget \\"' + widgetKey7 + '\\"')}
w7 = new W7(owner, props7);
context.__owl__.cmap[7] = w7.__owl__.id;
def6 = w7._prepare();
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: 7, hook: {insert(vn) {let nvn=w7._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c4[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
} else {
def6 = def6 || w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c4[_5_index]=pvnode;});
}
extra.promises.push(def6);
//WIDGET
let def9;
let w10 = 10 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[10]] : false;
let _8_index = c4.length;
const patchQueue10 = [];
c4.push(w10 && w10.__owl__.pvnode || null);
let props10 = {val:context['state'].val};
if (w10 && w10.__owl__.renderPromise && !w10.__owl__.vnode) {
if (utils.shallowEqual(props10, w10.__owl__.renderProps)) {
def9 = w10.__owl__.renderPromise;
} else {
w10.destroy();
w10 = false;
}
}
if (!w10) {
let widgetKey10 = \`AsyncChild\`;
let W10 = context.widgets && context.widgets[widgetKey10] || QWeb.widgets[widgetKey10];
if (!W10) {throw new Error('Cannot find the definition of widget \\"' + widgetKey10 + '\\"')}
w10 = new W10(owner, props10);
context.__owl__.cmap[10] = w10.__owl__.id;
def9 = w10._prepare();
def9 = def9.then(vnode=>{let pvnode=h(vnode.sel, {key: 10, hook: {insert(vn) {let nvn=w10._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w10.destroy();}}});c4[_8_index]=pvnode;w10.__owl__.pvnode = pvnode;});
} else {
def9 = def9 || w10._updateProps(props10, extra.forceUpdate, patchQueue10);
def9 = def9.then(()=>{if (w10.__owl__.isDestroyed) {return};let pvnode=w10.__owl__.pvnode;c4[_8_index]=pvnode;});
}
def9.then(w10._applyPatchQueue.bind(w10, patchQueue10));
return vn1;
}"
`;
exports[`class and style attributes with t-widget dynamic t-att-style is properly added and updated on widget root el 1`] = `
"function anonymous(context,extra
) {
@@ -10,11 +253,11 @@ exports[`class and style attributes with t-widget dynamic t-att-style is properl
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
const _5 = context['state'].style;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -51,11 +294,11 @@ exports[`class and style attributes with t-widget t-att-class is properly added/
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
const _5 = {a:context['state'].a,b:context['state'].b};
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -112,10 +355,10 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
context.number_value = _4[i];
//WIDGET
let key8 = context['number'];
let _5_index = c1.length;
c1.push(null);
let def6;
let w7 = key8 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key8]] : false;
let _5_index = c1.length;
c1.push(null);
let props7 = {};
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
@@ -153,10 +396,10 @@ exports[`composition t-widget with dynamic value 1`] = `
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -193,10 +436,10 @@ exports[`composition t-widget with dynamic value 2 1`] = `
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -233,10 +476,10 @@ exports[`other directives with t-widget t-on with handler bound to argument 1`]
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -273,10 +516,10 @@ exports[`other directives with t-widget t-on with handler bound to empty object
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -313,10 +556,10 @@ exports[`other directives with t-widget t-on with handler bound to empty object
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -353,10 +596,10 @@ exports[`other directives with t-widget t-on with handler bound to object 1`] =
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -393,10 +636,10 @@ exports[`other directives with t-widget t-on with prevent and self modifiers (or
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -433,10 +676,10 @@ exports[`other directives with t-widget t-on with self and prevent modifiers (or
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -473,10 +716,10 @@ exports[`other directives with t-widget t-on with self modifier 1`] = `
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -513,10 +756,10 @@ exports[`other directives with t-widget t-on with stop and/or prevent modifiers
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -554,10 +797,10 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
var vn1 = h('div', p1, c1);
//WIDGET
let key5 = 'somestring';
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = key5 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key5]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {flag:context['state'].flag};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -594,10 +837,10 @@ exports[`t-slot directive can define and call slots 1`] = `
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
+188
View File
@@ -2352,6 +2352,194 @@ describe("async rendering", () => {
await nextTick();
expect(destroyCount).toBe(0);
});
test("delayed t-widget with t-async directive", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<t t-widget="Child" val="state.val"/>
<t t-widget="AsyncChild" t-async="1" val="state.val"/>
</div>
</div>
<span t-name="Child"><t t-esc="props.val"/></span>
</templates>
`);
let def;
class Parent extends Widget {
widgets = { Child, AsyncChild };
state = { val: 0 };
updateApp() {
this.state.val++;
}
}
class Child extends Widget {}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>0</span><span>0</span>"
);
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>1</span><span>0</span>"
);
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>1</span><span>1</span>"
);
});
test("fast t-widget with t-async directive", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<t t-widget="Child" t-async="1" val="state.val"/>
<t t-widget="AsyncChild" val="state.val"/>
</div>
</div>
<span t-name="Child"><t t-esc="props.val"/></span>
</templates>
`);
let def;
class Parent extends Widget {
widgets = { Child, AsyncChild };
state = { val: 0 };
updateApp() {
this.state.val++;
}
}
class Child extends Widget {}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>0</span><span>0</span>"
);
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>1</span><span>0</span>"
);
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>1</span><span>1</span>"
);
});
test("t-widget with t-async directive: mixed re-renderings", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<t t-widget="Child" val="state.val"/>
<t t-widget="AsyncChild" t-async="1" val="state.val"/>
</div>
</div>
<span t-name="Child" t-on-click="increment">
<t t-esc="state.val"/>/<t t-esc="props.val"/>
</span>
</templates>
`);
let def;
class Parent extends Widget {
widgets = { Child, AsyncChild };
state = { val: 0 };
updateApp() {
this.state.val++;
}
}
class Child extends Widget {
state = { val: 0 };
increment() {
this.state.val++;
}
}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>0/0</span><span>0/0</span>"
);
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>0/1</span><span>0/0</span>"
);
// click on each Child to increment their local counter
const children = parent.el!.querySelectorAll("span");
children[0]!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>1/1</span><span>0/0</span>"
);
children[1]!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>1/1</span><span>1/0</span>"
);
// finalize first re-rendering (coming from the props update)
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>1/1</span><span>1/1</span>"
);
});
});
describe("updating environment", () => {
+94
View File
@@ -1196,6 +1196,94 @@ const SLOTS_CSS = `.main {
border-top: 1px solid white;
}`;
const ASYNC_COMPONENTS = `// This example will not work if your browser does not support ESNext class fields
// In this example, we have 2 sub widgets, one of them being async. However, we don't want renderings
// of the sync sub widget to be delayed because of the async one. We use the 't-async' directive for
// this purpose.
class App extends owl.Component {
widgets = {AsyncChild, NotificationManager};
state = { value: 0 };
increment() {
this.state.value++;
this.refs.notificationManager.notify("Value will be set to " + this.state.value);
}
}
class AsyncChild extends owl.Component {
willUpdateProps() {
// simulate a widget that needs to perform async stuff (e.g. an RPC)
// with the updated props before re-rendering itself
return new Promise(function (resolve) {
setTimeout(resolve, 1000);
});
}
}
class NotificationManager extends owl.Component {
state = { notifs: [] };
notify(notif) {
this.state.notifs.push(notif);
setTimeout(() => {
var index = this.state.notifs.indexOf(notif);
this.state.notifs.splice(index, 1);
}, 3000);
}
}
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
app.mount(document.body);`;
const ASYNC_COMPONENTS_XML = `<templates>
<div t-name="App" class="app">
<button t-on-click="increment">Increment</button>
<t t-widget="AsyncChild" t-async="1" value="state.value"/>
<t t-widget="NotificationManager" t-ref="notificationManager"/>
</div>
<div class="value" t-name="AsyncChild">
Current value: <t t-esc="props.value"/>
</div>
<div class="notification_manager" t-name="NotificationManager">
<t t-foreach="state.notifs" t-as="notif">
<div class="notification"><t t-esc="notif"/></div>
</t>
</div>
</templates>`;
const ASYNC_COMPONENTS_CSS = `.app {
width: 70%;
}
button {
color: darkred;
font-size: 30px;
width: 220px;
}
.value {
font-size: 26px;
padding: 20px;
}
.notification_manager {
position: absolute;
top: 0;
right: 0;
}
.notification {
width: 150px;
margin: 4px 8px;
padding: 16px;
border: 1px solid: black;
background-color: lightgray;
}`;
const EMPTY = `class App extends owl.Component {
}
@@ -1252,6 +1340,12 @@ export const SAMPLES = [
xml: SLOTS_XML,
css: SLOTS_CSS
},
{
description: "Asynchronous components",
code: ASYNC_COMPONENTS,
xml: ASYNC_COMPONENTS_XML,
css: ASYNC_COMPONENTS_CSS
},
{
description: "Empty",
code: EMPTY