diff --git a/doc/component.md b/doc/component.md index 10174103..b14fe2e2 100644 --- a/doc/component.md +++ b/doc/component.md @@ -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 +
+ + +
+ ``` diff --git a/doc/qweb.md b/doc/qweb.md index 7f026de2..59226d50 100644 --- a/doc/qweb.md +++ b/doc/qweb.md @@ -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 diff --git a/src/component.ts b/src/component.ts index 8c5614ad..ab6d43b7 100644 --- a/src/component.ts +++ b/src/component.ts @@ -274,20 +274,7 @@ export class Component { 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(patchQueue); } } @@ -568,6 +555,29 @@ export class Component { return 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 diff --git a/src/qweb_extensions.ts b/src/qweb_extensions.ts index 072138f2..27637db9 100644 --- a/src/qweb_extensions.ts +++ b/src/qweb_extensions.ts @@ -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") || diff --git a/tests/__snapshots__/animations.test.ts.snap b/tests/__snapshots__/animations.test.ts.snap index b581c515..4b31d32c 100644 --- a/tests/__snapshots__/animations.test.ts.snap +++ b/tests/__snapshots__/animations.test.ts.snap @@ -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)) { diff --git a/tests/__snapshots__/component.test.ts.snap b/tests/__snapshots__/component.test.ts.snap index 5a83ecaa..c0bf79f3 100644 --- a/tests/__snapshots__/component.test.ts.snap +++ b/tests/__snapshots__/component.test.ts.snap @@ -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)) { diff --git a/tests/component.test.ts b/tests/component.test.ts index c4251729..c8b825c5 100644 --- a/tests/component.test.ts +++ b/tests/component.test.ts @@ -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(` + +
+ +
+ + +
+
+ +
+ `); + + 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( + "00" + ); + + // click on button to increment Parent counter + def = makeDeferred(); + fixture.querySelector("button")!.click(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe( + "10" + ); + + def.resolve(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe( + "11" + ); + }); + + test("fast t-widget with t-async directive", async () => { + env.qweb.addTemplates(` + +
+ +
+ + +
+
+ +
+ `); + + 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( + "00" + ); + + // click on button to increment Parent counter + def = makeDeferred(); + fixture.querySelector("button")!.click(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe( + "10" + ); + + def.resolve(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe( + "11" + ); + }); + + test("t-widget with t-async directive: mixed re-renderings", async () => { + env.qweb.addTemplates(` + +
+ +
+ + +
+
+ + / + +
+ `); + + 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( + "0/00/0" + ); + + // click on button to increment Parent counter + def = makeDeferred(); + fixture.querySelector("button")!.click(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe( + "0/10/0" + ); + + // 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( + "1/10/0" + ); + + children[1]!.click(); + await nextTick(); + expect(fixture.querySelector(".children")!.innerHTML).toBe( + "1/11/0" + ); + + // finalize first re-rendering (coming from the props update) + def.resolve(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe( + "1/11/1" + ); + }); }); describe("updating environment", () => { diff --git a/tools/playground/samples.js b/tools/playground/samples.js index 08d3f6ba..99af2c12 100644 --- a/tools/playground/samples.js +++ b/tools/playground/samples.js @@ -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 = ` +
+ + + +
+ +
+ Current value: +
+ +
+ +
+
+
+
`; + +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