From 61f21511a5bea7a74caf1e6beb8d74c402b162cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 17 May 2019 10:49:04 +0200 Subject: [PATCH] [DOC] component: add detailed explanation for t-widget --- src/qweb_extensions.ts | 146 ++++++++++++++++++++- tests/__snapshots__/component.test.ts.snap | 10 +- 2 files changed, 150 insertions(+), 6 deletions(-) diff --git a/src/qweb_extensions.ts b/src/qweb_extensions.ts index f6bef574..a8e15f9d 100644 --- a/src/qweb_extensions.ts +++ b/src/qweb_extensions.ts @@ -162,6 +162,150 @@ QWeb.addDirective({ //------------------------------------------------------------------------------ // t-widget //------------------------------------------------------------------------------ + +/** + * The t-widget directive is certainly a complicated and hard to maintain piece + * of code. To help you, fellow developer, if you have to maintain it, I offer + * you this advice: Good luck... + * + * Since it is not 'direct' code, but rather code that generates other code, it + * is not easy to understand. To help you, here is a detailed and commented + * explanation of the code generated by the t-widget directive for the following + * situation: + * ```xml + * + * ``` + * + * ```js + * // this is the virtual node representing the parent div + * let c1 = [], p1 = { key: 1 }; + * var vn1 = h("div", p1, c1); + * + * // 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 deferred that will contain later either the new widget + * // creation, or the props update... + * let def3; + * + * // this is kind of tricky: we need here to find if the widget was already + * // created by a previous rendering. This is done by checking the internal + * // `cmap` (children map) of the parent widget: it maps keys to widget ids, + * // and, then, if there is an id, we look into the children list to get the + * // instance + * let w4 = + * key5 in context.__owl__.cmap + * ? context.__owl__.children[context.__owl__.cmap[key5]] + * : false; + * + * // 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 + * let props4 = { flag: context["state"].flag }; + * + * // If we have a widget, currently rendering, but not ready yet, and which was + * // rendered with different props, we do not want to wait for it to be ready, + * // then update it. We simply destroy it, and start anew. + * if ( + * w4 && + * w4.__owl__.renderPromise && + * !w4.__owl__.isStarted && + * props4 !== w4.__owl__.renderProps + * ) { + * w4.destroy(); + * w4 = false; + * } + * + * if (!w4) { + * // in this situation, we need to create a new widget. First step is + * // to get a reference to the class, then create an instance with + * // current context as parent, and the props. + * let W4 = context.widgets["child"]; + * if (!W4) { + * throw new Error("Cannot find the definition of widget 'child'"); + * } + * w4 = new W4(owner, props4); + * + * // Whenever we rerender the parent widget, we need to be sure that we + * // are able to find the widget instance. To do that, we register it to + * // the parent cmap (children map). Note that the 'template' key is + * // used here, since this is what identify the widget from the template + * // perspective. + * context.__owl__.cmap[key5] = w4.__owl__.id; + * + * // _prepare is called, to basically call willStart, then render the + * // widget + * def3 = w4._prepare(); + * + * def3 = def3.then(vnode => { + * // we create here a virtual node for the parent (NOT the widget). This + * // means that the vdom of the parent will be stopped here, and from + * // the parent's perspective, it simply is a vnode with no children. + * // However, it shares the same dom element with the component root + * // vnode. + * let pvnode = h(vnode.sel, { key: key5 }); + * + * // we add hooks to the parent vnode so we can interact with the new + * // widget at the proper time + * pvnode.data.hook = { + * insert(vn) { + * // the _mount method will patch the widget vdom into the elm vn.elm, + * // then call the mounted hooks. However, suprisingly, the snabbdom + * // patch method actually replace the elm by a new elm, so we need + * // to synchronise the pvnode elm with the resulting elm + * let nvn = w4._mount(vnode, vn.elm); + * pvnode.elm = nvn.elm; + * }, + * remove() { + * // apparently, in some cases, it is necessary to call the destroy + * // method here + * w4.destroy(); + * }, + * destroy() { + * // and here... + * w4.destroy(); + * } + * }; + * // the pvnode is inserted at the correct position in the div's children + * c1[_2_index] = pvnode; + * + * // we keep here a reference to the parent vnode (representing the + * // widget, so we can reuse it later whenever we update the widget + * w4.__owl__.pvnode = pvnode; + * }); + * } else { + * // this is the 'update' path of the directive. + * // the call to _updateProps is the actual widget update + * def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); + * def3 = def3.then(() => { + * // if widget was destroyed in the meantime, we do nothing (so, this + * // means that the parent's element children list will have a null in + * // the widget's position, which will cause the pvnode to be removed + * // when it is patched. + * if (w4.__owl__.isDestroyed) { + * return; + * } + * // like above, we register the pvnode to the children list, so it + * // will not be patched out of the dom. + * let pvnode = w4.__owl__.pvnode; + * c1[_2_index] = pvnode; + * }); + * } + * + * // we register the deferred here so the parent can coordinate its patch operation + * // with all the children. + * extra.promises.push(def3); + * return vn1; + * ``` + */ + QWeb.addDirective({ name: "widget", extraNames: ["props", "keepalive"], @@ -285,7 +429,7 @@ QWeb.addDirective({ } ctx.addLine(`def${defID} = w${widgetID}._prepare();`); 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, vn.elm);pvnode.elm=nvn.elm;${refExpr}},remove(){${finalizeWidgetCode}},destroy(){${finalizeWidgetCode}}}});c${ + `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}},remove(){${finalizeWidgetCode}},destroy(){${finalizeWidgetCode}}}});c${ ctx.parentNode }[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});` ); diff --git a/tests/__snapshots__/component.test.ts.snap b/tests/__snapshots__/component.test.ts.snap index 6f9c0171..4cfe57b6 100644 --- a/tests/__snapshots__/component.test.ts.snap +++ b/tests/__snapshots__/component.test.ts.snap @@ -24,7 +24,7 @@ exports[`class and style attributes with t-widget dynamic t-att-style is properl w4 = new W4(owner, props4); context.__owl__.cmap[4] = w4.__owl__.id; def3 = w4._prepare(); - def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.style = _5}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); + def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.style = _5}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn){let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); } else { def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};w4.el.style=_5;let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;}); @@ -62,7 +62,7 @@ exports[`class and style attributes with t-widget t-att-class is properly added/ if (_5[k]) { vn.elm.classList.add(k); } - }}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); + }}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn){let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); } else { def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let cl=w4.el.classList;for (let k in _5) {if (_5[k]) {cl.add(k)} else {cl.remove(k)}}let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;}); @@ -109,7 +109,7 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = ` w7 = new W7(owner, props7); context.__owl__.cmap[key8] = w7.__owl__.id; def6 = w7._prepare(); - def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8, hook: {insert(vn){let nvn=w7._mount(vnode, vn.elm);pvnode.elm=nvn.elm;},remove(){w7.destroy()},destroy(){w7.destroy()}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;}); + def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8, hook: {insert(vn){let nvn=w7._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove(){w7.destroy()},destroy(){w7.destroy()}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;}); } else { def6 = w7._updateProps(props7, extra.forceUpdate, extra.patchQueue); def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c1[_5_index]=pvnode;}); @@ -144,7 +144,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = ` w4 = new W4(owner, props4); context.__owl__.cmap[key5] = w4.__owl__.id; def3 = w4._prepare(); - def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5, hook: {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); + def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5, hook: {insert(vn){let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); } else { def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;}); @@ -177,7 +177,7 @@ exports[`random stuff/miscellaneous t-props should not be undefined (snapshottin w4 = new W4(owner, props4); context.__owl__.cmap[4] = w4.__owl__.id; def3 = w4._prepare(); - def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); + def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn){let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); } else { def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});