mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[REF] asyncroot: create asyncroot component
This component replaces the t-asyncroot directive.
This commit is contained in:
+5
-3
@@ -1164,8 +1164,8 @@ Here are a few tips on how to work with asynchronous components:
|
|||||||
3. Lazy loading external libraries is a good use case for async rendering. This
|
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
|
is mostly fine, because we can assume that it will only takes a fraction of a
|
||||||
second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs))
|
second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs))
|
||||||
4. For all the other cases, the `t-asyncroot` directive (to use alongside
|
4. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When
|
||||||
`t-component`) is there to help you. When this directive is met, a new rendering
|
this component is met, a new rendering
|
||||||
sub tree is created, such that the rendering of that component (and its
|
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
|
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
|
be used on an asynchronous component, to prevent it from delaying the
|
||||||
@@ -1177,7 +1177,9 @@ Here are a few tips on how to work with asynchronous components:
|
|||||||
```xml
|
```xml
|
||||||
<div t-name="ParentComponent">
|
<div t-name="ParentComponent">
|
||||||
<SyncChild />
|
<SyncChild />
|
||||||
<AsyncChild t-asyncroot="1"/>
|
<AsyncRoot>
|
||||||
|
<AsyncChild/>
|
||||||
|
</AsyncRoot>
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
# 🦉 Miscellaneous 🦉
|
||||||
|
|
||||||
|
## `AsyncRoot`
|
||||||
|
|
||||||
|
When this component is used, 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="ParentComponent">
|
||||||
|
<SyncChild />
|
||||||
|
<AsyncRoot>
|
||||||
|
<AsyncChild/>
|
||||||
|
</AsyncRoot>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `AsyncRoot` assumes that there is exactly one root node inside it. It can
|
||||||
|
be a dom node or a component.
|
||||||
@@ -29,6 +29,8 @@ owl
|
|||||||
useStore
|
useStore
|
||||||
useDispatch
|
useDispatch
|
||||||
useGetters
|
useGetters
|
||||||
|
misc
|
||||||
|
AsyncRoot
|
||||||
router
|
router
|
||||||
Link
|
Link
|
||||||
RouteComponent
|
RouteComponent
|
||||||
@@ -53,6 +55,7 @@ Note that for convenience, the `useState` hook is also exported at the root of t
|
|||||||
- [Context](context.md)
|
- [Context](context.md)
|
||||||
- [Event Bus](event_bus.md)
|
- [Event Bus](event_bus.md)
|
||||||
- [Hooks](hooks.md)
|
- [Hooks](hooks.md)
|
||||||
|
- [Misc](misc.md)
|
||||||
- [Observer](observer.md)
|
- [Observer](observer.md)
|
||||||
- [QWeb](qweb.md)
|
- [QWeb](qweb.md)
|
||||||
- [Router](router.md)
|
- [Router](router.md)
|
||||||
|
|||||||
+12
-25
@@ -186,7 +186,7 @@ QWeb.utils.defineProxy = function defineProxy(target, source) {
|
|||||||
|
|
||||||
QWeb.addDirective({
|
QWeb.addDirective({
|
||||||
name: "component",
|
name: "component",
|
||||||
extraNames: ["props", "keepalive", "asyncroot"],
|
extraNames: ["props", "keepalive"],
|
||||||
priority: 100,
|
priority: 100,
|
||||||
atNodeEncounter({ ctx, value, node, qweb }): boolean {
|
atNodeEncounter({ ctx, value, node, qweb }): boolean {
|
||||||
ctx.addLine("//COMPONENT");
|
ctx.addLine("//COMPONENT");
|
||||||
@@ -196,7 +196,6 @@ QWeb.addDirective({
|
|||||||
ctx.rootContext.shouldDefineUtils = true;
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
|
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
|
||||||
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
|
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
|
||||||
let async = node.getAttribute("t-asyncroot") ? true : false;
|
|
||||||
|
|
||||||
// t-on- events and t-transition
|
// t-on- events and t-transition
|
||||||
const events: [string, string[], string, string][] = [];
|
const events: [string, string[], string, string][] = [];
|
||||||
@@ -355,26 +354,20 @@ QWeb.addDirective({
|
|||||||
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
|
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
|
||||||
}
|
}
|
||||||
let shouldProxy = false;
|
let shouldProxy = false;
|
||||||
if (async || keepAlive) {
|
if (keepAlive) {
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`
|
`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (async) {
|
if (ctx.parentNode) {
|
||||||
ctx.addLine(
|
ctx.addLine(`c${ctx.parentNode}.push(null);`);
|
||||||
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
if (ctx.parentNode) {
|
let id = ctx.generateID();
|
||||||
ctx.addLine(`c${ctx.parentNode}.push(null);`);
|
ctx.rootContext.rootNode = id;
|
||||||
} else {
|
shouldProxy = true;
|
||||||
let id = ctx.generateID();
|
ctx.rootContext.shouldDefineResult = true;
|
||||||
ctx.rootContext.rootNode = id;
|
ctx.addLine(`let vn${id} = {};`);
|
||||||
shouldProxy = true;
|
ctx.addLine(`result = vn${id};`);
|
||||||
ctx.rootContext.shouldDefineResult = true;
|
|
||||||
ctx.addLine(`let vn${id} = {};`);
|
|
||||||
ctx.addLine(`result = vn${id};`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (hasDynamicProps) {
|
if (hasDynamicProps) {
|
||||||
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
|
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
|
||||||
@@ -469,7 +462,7 @@ QWeb.addDirective({
|
|||||||
|
|
||||||
ctx.addElse();
|
ctx.addElse();
|
||||||
// need to update component
|
// need to update component
|
||||||
let patchQueueCode = async || keepAlive ? `fiber${componentID}` : "extra.fiber";
|
let patchQueueCode = keepAlive ? `fiber${componentID}` : "extra.fiber";
|
||||||
if (keepAlive) {
|
if (keepAlive) {
|
||||||
// if we have t-keepalive="1", the component could be unmounted, but then
|
// if we have t-keepalive="1", the component could be unmounted, but then
|
||||||
// we __updateProps is called. This is ok, but we do not want to call
|
// we __updateProps is called. This is ok, but we do not want to call
|
||||||
@@ -499,13 +492,7 @@ QWeb.addDirective({
|
|||||||
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
|
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (async) {
|
ctx.addLine(`extra.promises.push(def${defID});`);
|
||||||
ctx.addLine(
|
|
||||||
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, fiber${componentID}));`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ctx.addLine(`extra.promises.push(def${defID});`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { QWeb } from "./qweb/index";
|
|||||||
import * as _store from "./store";
|
import * as _store from "./store";
|
||||||
import * as _utils from "./utils";
|
import * as _utils from "./utils";
|
||||||
import * as _tags from "./tags";
|
import * as _tags from "./tags";
|
||||||
|
import {AsyncRoot} from "./misc/async_root";
|
||||||
import * as _hooks from "./hooks";
|
import * as _hooks from "./hooks";
|
||||||
import * as _context from "./context";
|
import * as _context from "./context";
|
||||||
import { Link } from "./router/link";
|
import { Link } from "./router/link";
|
||||||
@@ -26,6 +27,7 @@ export const router = { Router, RouteComponent, Link };
|
|||||||
export const Store = _store.Store;
|
export const Store = _store.Store;
|
||||||
export const utils = _utils;
|
export const utils = _utils;
|
||||||
export const tags = _tags;
|
export const tags = _tags;
|
||||||
|
export const misc = { AsyncRoot};
|
||||||
export const hooks = Object.assign({}, _hooks, {
|
export const hooks = Object.assign({}, _hooks, {
|
||||||
useContext: _context.useContext,
|
useContext: _context.useContext,
|
||||||
useDispatch: _store.useDispatch,
|
useDispatch: _store.useDispatch,
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Component } from "../component/component";
|
||||||
|
import { xml } from "../tags";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AsyncRoot
|
||||||
|
*
|
||||||
|
* Owl is by default asynchronous, and the user interface will wait for all its
|
||||||
|
* subcomponents to be rendered before updating the DOM. This is most of the
|
||||||
|
* time what we want, but in some cases, it makes sense to "detach" a component
|
||||||
|
* from this coordination. This is the goal of the AsyncRoot component.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class AsyncRoot extends Component<any, any> {
|
||||||
|
static template = xml`<t t-slot="default"/>`;
|
||||||
|
|
||||||
|
async __updateProps(nextProps, parentFiber) {
|
||||||
|
this.render(parentFiber.force);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,248 +1,5 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`async rendering delayed component with t-asyncroot directive 1`] = `
|
|
||||||
"function anonymous(context,extra
|
|
||||||
) {
|
|
||||||
let utils = this.constructor.utils;
|
|
||||||
let QWeb = this.constructor;
|
|
||||||
let parent = context;
|
|
||||||
let owner = context;
|
|
||||||
var h = this.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);
|
|
||||||
extra.handlers['click' + 2] = extra.handlers['click' + 2] || function (e) {const fn = context['updateApp'];if (fn) { fn.call(owner, e); } else { context.updateApp; }};
|
|
||||||
p2.on['click'] = extra.handlers['click' + 2];
|
|
||||||
c2.push({text: \`Update App State\`});
|
|
||||||
let _4 = {'children':true};
|
|
||||||
let c5 = [], p5 = {key:5,class:_4};
|
|
||||||
var vn5 = h('div', p5, c5);
|
|
||||||
c1.push(vn5);
|
|
||||||
//COMPONENT
|
|
||||||
let def7;
|
|
||||||
let templateId8 = \`__9__\`;
|
|
||||||
let w8 = templateId8 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId8]] : false;
|
|
||||||
let _6_index = c5.length;
|
|
||||||
c5.push(null);
|
|
||||||
let props8 = {val:context['state'].val};
|
|
||||||
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) {
|
|
||||||
if (utils.shallowEqual(props8, w8.__owl__.currentFiber.props)) {
|
|
||||||
def7 = w8.__owl__.currentFiber.promise;
|
|
||||||
} else {
|
|
||||||
w8.destroy();
|
|
||||||
w8 = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!w8) {
|
|
||||||
let componentKey8 = \`Child\`;
|
|
||||||
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| context['Child'];
|
|
||||||
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
|
|
||||||
w8 = new W8(parent, props8);
|
|
||||||
parent.__owl__.cmap[templateId8] = w8.__owl__.id;
|
|
||||||
def7 = w8.__prepare(extra.fiber, undefined, undefined);
|
|
||||||
def7 = def7.then(vnode=>{if (w8.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId8, hook: {insert(vn) {let nvn=w8.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w8.destroy();}}});c5[_6_index]=pvnode;w8.__owl__.pvnode = pvnode;});
|
|
||||||
} else {
|
|
||||||
def7 = def7 || w8.__updateProps(props8, extra.fiber, undefined, undefined);
|
|
||||||
def7 = def7.then(()=>{if (w8.__owl__.isDestroyed) {return};let pvnode=w8.__owl__.pvnode;c5[_6_index]=pvnode;});
|
|
||||||
}
|
|
||||||
extra.promises.push(def7);
|
|
||||||
//COMPONENT
|
|
||||||
let def11;
|
|
||||||
let templateId12 = \`__13__\`;
|
|
||||||
let w12 = templateId12 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId12]] : false;
|
|
||||||
let _10_index = c5.length;
|
|
||||||
const fiber12 = Object.assign(Object.create(extra.fiber), {patchQueue: []});
|
|
||||||
c5.push(w12 && w12.__owl__.pvnode || null);
|
|
||||||
let props12 = {val:context['state'].val};
|
|
||||||
if (w12 && w12.__owl__.currentFiber && !w12.__owl__.vnode) {
|
|
||||||
if (utils.shallowEqual(props12, w12.__owl__.currentFiber.props)) {
|
|
||||||
def11 = w12.__owl__.currentFiber.promise;
|
|
||||||
} else {
|
|
||||||
w12.destroy();
|
|
||||||
w12 = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!w12) {
|
|
||||||
let componentKey12 = \`AsyncChild\`;
|
|
||||||
let W12 = context.constructor.components[componentKey12] || QWeb.components[componentKey12]|| context['AsyncChild'];
|
|
||||||
if (!W12) {throw new Error('Cannot find the definition of component \\"' + componentKey12 + '\\"')}
|
|
||||||
w12 = new W12(parent, props12);
|
|
||||||
parent.__owl__.cmap[templateId12] = w12.__owl__.id;
|
|
||||||
def11 = w12.__prepare(extra.fiber, undefined, undefined);
|
|
||||||
def11 = def11.then(vnode=>{if (w12.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId12, hook: {insert(vn) {let nvn=w12.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w12.destroy();}}});c5[_10_index]=pvnode;w12.__owl__.pvnode = pvnode;});
|
|
||||||
} else {
|
|
||||||
def11 = def11 || w12.__updateProps(props12, fiber12, undefined, undefined);
|
|
||||||
def11 = def11.then(()=>{if (w12.__owl__.isDestroyed) {return};let pvnode=w12.__owl__.pvnode;c5[_10_index]=pvnode;});
|
|
||||||
}
|
|
||||||
def11.then(w12.__applyPatchQueue.bind(w12, fiber12));
|
|
||||||
return vn1;
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`async rendering fast component with t-asyncroot directive 1`] = `
|
|
||||||
"function anonymous(context,extra
|
|
||||||
) {
|
|
||||||
let utils = this.constructor.utils;
|
|
||||||
let QWeb = this.constructor;
|
|
||||||
let parent = context;
|
|
||||||
let owner = context;
|
|
||||||
var h = this.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);
|
|
||||||
extra.handlers['click' + 2] = extra.handlers['click' + 2] || function (e) {const fn = context['updateApp'];if (fn) { fn.call(owner, e); } else { context.updateApp; }};
|
|
||||||
p2.on['click'] = extra.handlers['click' + 2];
|
|
||||||
c2.push({text: \`Update App State\`});
|
|
||||||
let _4 = {'children':true};
|
|
||||||
let c5 = [], p5 = {key:5,class:_4};
|
|
||||||
var vn5 = h('div', p5, c5);
|
|
||||||
c1.push(vn5);
|
|
||||||
//COMPONENT
|
|
||||||
let def7;
|
|
||||||
let templateId8 = \`__9__\`;
|
|
||||||
let w8 = templateId8 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId8]] : false;
|
|
||||||
let _6_index = c5.length;
|
|
||||||
const fiber8 = Object.assign(Object.create(extra.fiber), {patchQueue: []});
|
|
||||||
c5.push(w8 && w8.__owl__.pvnode || null);
|
|
||||||
let props8 = {val:context['state'].val};
|
|
||||||
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) {
|
|
||||||
if (utils.shallowEqual(props8, w8.__owl__.currentFiber.props)) {
|
|
||||||
def7 = w8.__owl__.currentFiber.promise;
|
|
||||||
} else {
|
|
||||||
w8.destroy();
|
|
||||||
w8 = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!w8) {
|
|
||||||
let componentKey8 = \`Child\`;
|
|
||||||
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| context['Child'];
|
|
||||||
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
|
|
||||||
w8 = new W8(parent, props8);
|
|
||||||
parent.__owl__.cmap[templateId8] = w8.__owl__.id;
|
|
||||||
def7 = w8.__prepare(extra.fiber, undefined, undefined);
|
|
||||||
def7 = def7.then(vnode=>{if (w8.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId8, hook: {insert(vn) {let nvn=w8.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w8.destroy();}}});c5[_6_index]=pvnode;w8.__owl__.pvnode = pvnode;});
|
|
||||||
} else {
|
|
||||||
def7 = def7 || w8.__updateProps(props8, fiber8, undefined, undefined);
|
|
||||||
def7 = def7.then(()=>{if (w8.__owl__.isDestroyed) {return};let pvnode=w8.__owl__.pvnode;c5[_6_index]=pvnode;});
|
|
||||||
}
|
|
||||||
def7.then(w8.__applyPatchQueue.bind(w8, fiber8));
|
|
||||||
//COMPONENT
|
|
||||||
let def11;
|
|
||||||
let templateId12 = \`__13__\`;
|
|
||||||
let w12 = templateId12 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId12]] : false;
|
|
||||||
let _10_index = c5.length;
|
|
||||||
c5.push(null);
|
|
||||||
let props12 = {val:context['state'].val};
|
|
||||||
if (w12 && w12.__owl__.currentFiber && !w12.__owl__.vnode) {
|
|
||||||
if (utils.shallowEqual(props12, w12.__owl__.currentFiber.props)) {
|
|
||||||
def11 = w12.__owl__.currentFiber.promise;
|
|
||||||
} else {
|
|
||||||
w12.destroy();
|
|
||||||
w12 = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!w12) {
|
|
||||||
let componentKey12 = \`AsyncChild\`;
|
|
||||||
let W12 = context.constructor.components[componentKey12] || QWeb.components[componentKey12]|| context['AsyncChild'];
|
|
||||||
if (!W12) {throw new Error('Cannot find the definition of component \\"' + componentKey12 + '\\"')}
|
|
||||||
w12 = new W12(parent, props12);
|
|
||||||
parent.__owl__.cmap[templateId12] = w12.__owl__.id;
|
|
||||||
def11 = w12.__prepare(extra.fiber, undefined, undefined);
|
|
||||||
def11 = def11.then(vnode=>{if (w12.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId12, hook: {insert(vn) {let nvn=w12.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w12.destroy();}}});c5[_10_index]=pvnode;w12.__owl__.pvnode = pvnode;});
|
|
||||||
} else {
|
|
||||||
def11 = def11 || w12.__updateProps(props12, extra.fiber, undefined, undefined);
|
|
||||||
def11 = def11.then(()=>{if (w12.__owl__.isDestroyed) {return};let pvnode=w12.__owl__.pvnode;c5[_10_index]=pvnode;});
|
|
||||||
}
|
|
||||||
extra.promises.push(def11);
|
|
||||||
return vn1;
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`async rendering t-component with t-asyncroot directive: mixed re-renderings 1`] = `
|
|
||||||
"function anonymous(context,extra
|
|
||||||
) {
|
|
||||||
let utils = this.constructor.utils;
|
|
||||||
let QWeb = this.constructor;
|
|
||||||
let parent = context;
|
|
||||||
let owner = context;
|
|
||||||
var h = this.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);
|
|
||||||
extra.handlers['click' + 2] = extra.handlers['click' + 2] || function (e) {const fn = context['updateApp'];if (fn) { fn.call(owner, e); } else { context.updateApp; }};
|
|
||||||
p2.on['click'] = extra.handlers['click' + 2];
|
|
||||||
c2.push({text: \`Update App State\`});
|
|
||||||
let _4 = {'children':true};
|
|
||||||
let c5 = [], p5 = {key:5,class:_4};
|
|
||||||
var vn5 = h('div', p5, c5);
|
|
||||||
c1.push(vn5);
|
|
||||||
//COMPONENT
|
|
||||||
let def7;
|
|
||||||
let templateId8 = \`__9__\`;
|
|
||||||
let w8 = templateId8 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId8]] : false;
|
|
||||||
let _6_index = c5.length;
|
|
||||||
c5.push(null);
|
|
||||||
let props8 = {val:context['state'].val};
|
|
||||||
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) {
|
|
||||||
if (utils.shallowEqual(props8, w8.__owl__.currentFiber.props)) {
|
|
||||||
def7 = w8.__owl__.currentFiber.promise;
|
|
||||||
} else {
|
|
||||||
w8.destroy();
|
|
||||||
w8 = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!w8) {
|
|
||||||
let componentKey8 = \`Child\`;
|
|
||||||
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| context['Child'];
|
|
||||||
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
|
|
||||||
w8 = new W8(parent, props8);
|
|
||||||
parent.__owl__.cmap[templateId8] = w8.__owl__.id;
|
|
||||||
def7 = w8.__prepare(extra.fiber, undefined, undefined);
|
|
||||||
def7 = def7.then(vnode=>{if (w8.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId8, hook: {insert(vn) {let nvn=w8.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w8.destroy();}}});c5[_6_index]=pvnode;w8.__owl__.pvnode = pvnode;});
|
|
||||||
} else {
|
|
||||||
def7 = def7 || w8.__updateProps(props8, extra.fiber, undefined, undefined);
|
|
||||||
def7 = def7.then(()=>{if (w8.__owl__.isDestroyed) {return};let pvnode=w8.__owl__.pvnode;c5[_6_index]=pvnode;});
|
|
||||||
}
|
|
||||||
extra.promises.push(def7);
|
|
||||||
//COMPONENT
|
|
||||||
let def11;
|
|
||||||
let templateId12 = \`__13__\`;
|
|
||||||
let w12 = templateId12 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId12]] : false;
|
|
||||||
let _10_index = c5.length;
|
|
||||||
const fiber12 = Object.assign(Object.create(extra.fiber), {patchQueue: []});
|
|
||||||
c5.push(w12 && w12.__owl__.pvnode || null);
|
|
||||||
let props12 = {val:context['state'].val};
|
|
||||||
if (w12 && w12.__owl__.currentFiber && !w12.__owl__.vnode) {
|
|
||||||
if (utils.shallowEqual(props12, w12.__owl__.currentFiber.props)) {
|
|
||||||
def11 = w12.__owl__.currentFiber.promise;
|
|
||||||
} else {
|
|
||||||
w12.destroy();
|
|
||||||
w12 = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!w12) {
|
|
||||||
let componentKey12 = \`AsyncChild\`;
|
|
||||||
let W12 = context.constructor.components[componentKey12] || QWeb.components[componentKey12]|| context['AsyncChild'];
|
|
||||||
if (!W12) {throw new Error('Cannot find the definition of component \\"' + componentKey12 + '\\"')}
|
|
||||||
w12 = new W12(parent, props12);
|
|
||||||
parent.__owl__.cmap[templateId12] = w12.__owl__.id;
|
|
||||||
def11 = w12.__prepare(extra.fiber, undefined, undefined);
|
|
||||||
def11 = def11.then(vnode=>{if (w12.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId12, hook: {insert(vn) {let nvn=w12.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w12.destroy();}}});c5[_10_index]=pvnode;w12.__owl__.pvnode = pvnode;});
|
|
||||||
} else {
|
|
||||||
def11 = def11 || w12.__updateProps(props12, fiber12, undefined, undefined);
|
|
||||||
def11 = def11.then(()=>{if (w12.__owl__.isDestroyed) {return};let pvnode=w12.__owl__.pvnode;c5[_10_index]=pvnode;});
|
|
||||||
}
|
|
||||||
def11.then(w12.__applyPatchQueue.bind(w12, fiber12));
|
|
||||||
return vn1;
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`basic widget properties reconciliation alg works for t-foreach in t-foreach 1`] = `
|
exports[`basic widget properties reconciliation alg works for t-foreach in t-foreach 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -2721,172 +2721,6 @@ describe("async rendering", () => {
|
|||||||
expect(destroyCount).toBe(0);
|
expect(destroyCount).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("delayed component with t-asyncroot directive", async () => {
|
|
||||||
env.qweb.addTemplates(`
|
|
||||||
<templates>
|
|
||||||
<div t-name="Parent">
|
|
||||||
<button t-on-click="updateApp">Update App State</button>
|
|
||||||
<div class="children">
|
|
||||||
<Child val="state.val"/>
|
|
||||||
<AsyncChild t-asyncroot="1" val="state.val"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span t-name="Child"><t t-esc="props.val"/></span>
|
|
||||||
</templates>
|
|
||||||
`);
|
|
||||||
|
|
||||||
let def;
|
|
||||||
class Child extends Widget {}
|
|
||||||
class AsyncChild extends Child {
|
|
||||||
willUpdateProps() {
|
|
||||||
return def;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class Parent extends Widget {
|
|
||||||
static components = { Child, AsyncChild };
|
|
||||||
state = useState({ val: 0 });
|
|
||||||
|
|
||||||
updateApp() {
|
|
||||||
this.state.val++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 component with t-asyncroot directive", async () => {
|
|
||||||
env.qweb.addTemplates(`
|
|
||||||
<templates>
|
|
||||||
<div t-name="Parent">
|
|
||||||
<button t-on-click="updateApp">Update App State</button>
|
|
||||||
<div class="children">
|
|
||||||
<Child t-asyncroot="1" val="state.val"/>
|
|
||||||
<AsyncChild val="state.val"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span t-name="Child"><t t-esc="props.val"/></span>
|
|
||||||
</templates>
|
|
||||||
`);
|
|
||||||
|
|
||||||
let def;
|
|
||||||
class Child extends Widget {}
|
|
||||||
class AsyncChild extends Child {
|
|
||||||
willUpdateProps() {
|
|
||||||
return def;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class Parent extends Widget {
|
|
||||||
static components = { Child, AsyncChild };
|
|
||||||
state = useState({ val: 0 });
|
|
||||||
|
|
||||||
updateApp() {
|
|
||||||
this.state.val++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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-component with t-asyncroot 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">
|
|
||||||
<Child val="state.val"/>
|
|
||||||
<AsyncChild t-asyncroot="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 Child extends Widget {
|
|
||||||
state = useState({ val: 0 });
|
|
||||||
|
|
||||||
increment() {
|
|
||||||
this.state.val++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class AsyncChild extends Child {
|
|
||||||
willUpdateProps() {
|
|
||||||
return def;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class Parent extends Widget {
|
|
||||||
static components = { Child, AsyncChild };
|
|
||||||
state = useState({ val: 0 });
|
|
||||||
|
|
||||||
updateApp() {
|
|
||||||
this.state.val++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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>");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("rendering component again in next microtick", async () => {
|
test("rendering component again in next microtick", async () => {
|
||||||
class Child extends Widget {}
|
class Child extends Widget {}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import { AsyncRoot } from "../../src/misc/async_root";
|
||||||
|
import { useState } from "../../src/hooks";
|
||||||
|
import { xml } from "../../src/tags";
|
||||||
|
import { makeDeferred, makeTestFixture, makeTestEnv, nextTick } from "../helpers";
|
||||||
|
import { Env, Component } from "../../src/component/component";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Setup and helpers
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// We create before each test:
|
||||||
|
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
||||||
|
// manipulations. Note that it is removed after each test.
|
||||||
|
// - env: a WEnv, necessary to create new components
|
||||||
|
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
let env: Env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
env = makeTestEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Asyncroot", () => {
|
||||||
|
test("delayed component with AsyncRoot component", async () => {
|
||||||
|
let def;
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="props.val"/></span>`;
|
||||||
|
}
|
||||||
|
class AsyncChild extends Child {
|
||||||
|
willUpdateProps() {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<button t-on-click="updateApp">Update App State</button>
|
||||||
|
<div class="children">
|
||||||
|
<Child val="state.val"/>
|
||||||
|
<AsyncRoot>
|
||||||
|
<AsyncChild val="state.val"/>
|
||||||
|
</AsyncRoot>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
static components = { Child, AsyncChild, AsyncRoot };
|
||||||
|
state = useState({ val: 0 });
|
||||||
|
|
||||||
|
updateApp() {
|
||||||
|
this.state.val++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent(env);
|
||||||
|
await parent.mount(fixture);
|
||||||
|
|
||||||
|
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 component with AsyncRoot", async () => {
|
||||||
|
let def;
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="props.val"/></span>`;
|
||||||
|
}
|
||||||
|
class AsyncChild extends Child {
|
||||||
|
willUpdateProps() {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<button t-on-click="updateApp">Update App State</button>
|
||||||
|
<div class="children">
|
||||||
|
<AsyncRoot>
|
||||||
|
<Child val="state.val"/>
|
||||||
|
</AsyncRoot>
|
||||||
|
<AsyncChild val="state.val"/>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
static components = { Child, AsyncChild, AsyncRoot };
|
||||||
|
state = useState({ val: 0 });
|
||||||
|
|
||||||
|
updateApp() {
|
||||||
|
this.state.val++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent(env);
|
||||||
|
await parent.mount(fixture);
|
||||||
|
|
||||||
|
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("asyncroot component: mixed re-renderings", async () => {
|
||||||
|
let def;
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<span t-on-click="increment">
|
||||||
|
<t t-esc="state.val"/>/<t t-esc="props.val"/>
|
||||||
|
</span>`;
|
||||||
|
state = useState({ val: 0 });
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.val++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class AsyncChild extends Child {
|
||||||
|
willUpdateProps() {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<button t-on-click="updateApp">Update App State</button>
|
||||||
|
<div class="children">
|
||||||
|
<Child val="state.val"/>
|
||||||
|
<AsyncRoot>
|
||||||
|
<AsyncChild val="state.val"/>
|
||||||
|
</AsyncRoot>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
static components = { Child, AsyncChild, AsyncRoot };
|
||||||
|
state = useState({ val: 0 });
|
||||||
|
|
||||||
|
updateApp() {
|
||||||
|
this.state.val++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent(env);
|
||||||
|
await parent.mount(fixture);
|
||||||
|
|
||||||
|
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>");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user