Compare commits

..

18 Commits

Author SHA1 Message Date
Géry Debongnie 0762eeb010 [REL] v1.0.0-beta2 2019-12-11 10:19:04 +01:00
Géry Debongnie bc455be720 [IMP] components: slots can now define default content
closes #554
2019-12-11 10:11:47 +01:00
Lucas Perais (lpe) 9145799ae9 [FIX] qweb: evaluate body of t-set immediately 2019-12-11 10:07:41 +01:00
Géry Debongnie ce9c2f8613 [FIX] qweb/component: refactoring of scoping/variables/slots
This commit is a significant refactoring of the internal of QWeb. It
simplifies the way variables/scoping and slots interact together.  The
main idea is that we use a simple scope object instead of a
context/var/scope object.

This commit also implement the actual correct QWeb semantic for the
t-call directive with a sub body.  Before, we simply extracted the
variables from the body and injected them at the top of the sub
template.  We now simply compile the body before the sub template.

This is a joint work with Lucas (lpe).

closes #541
closes #544
closes #545

closes #557
closes #556
2019-12-11 10:07:41 +01:00
Géry Debongnie b890e7ceae [IMP] qweb: add template name in generated compiled code 2019-12-10 16:14:09 +01:00
Géry Debongnie 124211c18b [DOC] clarify store documentation on connected components
closes #538
2019-12-06 12:25:55 +01:00
Géry Debongnie be8c3bbf6f [IMP] tooling: add warning if component not mounted, but rendered
closes #551
2019-12-06 12:07:54 +01:00
Géry Debongnie e162a6fb6c [IMP] qweb: add support for arrow functions in expression parser
closes #542
2019-12-06 11:49:55 +01:00
Géry Debongnie 7ec05ff8cf [IMP] component: add position option to mount method
A component can now be mounted in different positions: either 'before' (at
the start), 'after' (at the end), or 'attach' (take possession of a target
element)

closes #539
2019-12-06 10:53:01 +01:00
Géry Debongnie a0bfbf6dd0 [FIX] qweb/component: do not call handlers for unmounted components
Unmounted components should be considered inactive, at least from the
perspective of Owl itself.

closes #543
2019-12-06 09:58:12 +01:00
Géry Debongnie b67cb71048 [FIX] slots: propagate vars to correct function
closes #529
2019-12-06 09:53:48 +01:00
Géry Debongnie f5b3ef5cf3 [DOC] add some information on variables and t-call
closes #540
2019-12-05 21:49:51 +01:00
Lucas Perais (lpe) ecfccf2448 [FIX] portal: manually unmounting works
Before this commit, when manually unmounting the portal
or one of its parents, the teleported elements were not cleaned
and stayed teleported forever

After this commit, the teleported content is brought back and
unmounted much like any component

closes #531
2019-12-04 09:48:08 +01:00
Géry Debongnie f12d3373c9 [FIX] qweb: properly handle empty class attribute
The classList.add method actually crashes when given an empty string (or
a string with just white spaces).

closes #530
2019-12-03 16:19:09 +01:00
Géry Debongnie 286090efca [FIX] tooling: debug tool logged too many scheduler stops
This was caused by additional calls to flush (because of connected
components)

closes #533
2019-12-03 15:16:29 +01:00
Géry Debongnie 2b4ff7b2fd [FIX] doc: the debugOwl helper was missing a parameter
closes #532
2019-12-03 14:13:42 +01:00
Géry Debongnie c43178a45b [CLEANUP] observer: remove useless variable 2019-12-03 11:17:37 +01:00
Géry Debongnie 033a431e18 [ADD] tools: add benchmark for 1.0.0-beta1, remove old ones 2019-12-03 09:12:52 +01:00
52 changed files with 6239 additions and 16086 deletions
+2 -2
View File
@@ -104,8 +104,8 @@ Submit a PR!
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.0.0-beta1.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta1/owl.js)
- [owl-1.0.0-beta1.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta1/owl.min.js)
- [owl-1.0.0-beta2.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta2/owl.js)
- [owl-1.0.0-beta2.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta2/owl.min.js)
Some npm scripts are available:
+29 -5
View File
@@ -234,12 +234,23 @@ component.
We explain here all the public methods of the `Component` class.
- **`mount(target)`** (async): this is the main way a
- **`mount(target, options)`** (async): this is the main way a
component is added to the DOM: the root component is mounted to a target
HTMLElement (or document fragment). Obviously, this is asynchronous, since each children need to be
created as well. Most applications will need to call `mount` exactly once, on
the root component.
The `options` argument is an optional object with a `position` key. The
`position` key can have three possible values: `first-child`, `last-child`, `self`.
- `first-child`: with this option, the component will be prepended inside the target,
- `last-child` (default value): with this option, the component will be
appended in the target element,
- `self`: the target will be used as the root element for the component. This
means that the target has to be an HTMLElement (and not a document fragment).
In this situation, it is possible that the component cannot be unmounted. For
example, if its target is `document.body`.
Note that if a component is mounted, unmounted and remounted, it will be
automatically re-rendered to ensure that changes in its state (or something
in the environment, or in the store, or ...) will be taken into account.
@@ -258,25 +269,25 @@ We explain here all the public methods of the `Component` class.
// app is now visible
```
- **`unmount()`**: in case a component needs to be detached/removed from the DOM, this
* **`unmount()`**: in case a component needs to be detached/removed from the DOM, this
method can be used. Most applications should not call `unmount`, this is more
useful to the underlying component system.
- **`render()`** (async): calling this method directly will cause a rerender. Note
* **`render()`** (async): calling this method directly will cause a rerender. Note
that this should be very rare to have to do it manually, the Owl framework is
most of the time responsible for doing that at an appropriate moment.
Note that the render method is asynchronous, so one cannot observe the updated
DOM in the same stack frame.
- **`shouldUpdate(nextProps)`**: this method is called each time a component's props
* **`shouldUpdate(nextProps)`**: this method is called each time a component's props
are updated. It returns a boolean, which indicates if the component should
ignore a props update. If it returns false, then `willUpdateProps` will not
be called, and no rendering will occur. Its default implementation is to
always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
can be useful if we are handling large number of components.
- **`destroy()`**. As its name suggests, this method will remove the component,
* **`destroy()`**. As its name suggests, this method will remove the component,
and perform all necessary cleanup, such as unmounting the component, its children,
removing the parent/children relationship. This method should almost never be
called directly (except maybe on the root component), but should be done by the
@@ -854,6 +865,19 @@ be considered the `default` slot. For example:
</div>
```
Slots can define a default content, in case the parent did not define them:
```xml
<div t-name="Parent">
<Child/>
</div>
<span t-name="Child">
<t t-slot="default">default content</t>
</span>
<!-- will be rendered as: <div><span>default content</span></div> -->
```
### Dynamic sub components
It is not common, but sometimes we need a dynamic component name. In this case,
@@ -500,6 +500,15 @@ will result in :
</div>
```
This can be used to define variables scoped to a sub template:
```xml
<t t-call="other-template">
<t t-set="var" t-value="1"/>
</t>
<!-- "var" does not exist here -->
```
### Translations
By default, QWeb specify that templates should be translated. If this behaviour
+2 -2
View File
@@ -25,8 +25,8 @@ component should own which part of the state.
Owl's solution to this issue is a centralized store. It is a class that owns
some (or all) state, and lets the developer update it in a structured way, with
`actions`. Owl components can then connect to the store, and will be updated if
necessary.
`actions`. Owl components can then connect to the store to read their relevant
state, and they will be rerendered if the state is updated.
Note: Owl store is inspired by React Redux and VueX.
+2 -2
View File
@@ -91,8 +91,8 @@ logging useful information is extremely valuable. There is a [javascript file](.
Once it is executed, it will log a lot of information on each component main hooks. The following code is a minified version to make it easier to copy/paste:
```
function debugOwl(o,t){let e,n="[OWL_DEBUG]";function l(o){let t=JSON.stringify(o||{});return t.length>200&&(t=t.slice(0,200)+"..."),t}if(Object.defineProperty(o.Component,"current",{get:()=>e,set(s){e=s;const c=s.constructor.name;if(t.componentBlackList&&t.componentBlackList.test(c))return;if(t.componentWhiteList&&!t.componentWhiteList.test(c))return;let r;Object.defineProperty(e,"__owl__",{get:()=>r,set(e){!function(e,s,c){let r=`${s}<id=${c}>`,i=o=>(!t.methodBlackList||!t.methodBlackList.includes(o))&&!(t.methodWhiteList&&!t.methodWhiteList.includes(o));i("constructor")&&console.log(`${n} ${r} constructor, props=${l(e.props)}`);i("willStart")&&o.hooks.onWillStart(()=>{console.log(`${n} ${r} willStart`)});i("mounted")&&o.hooks.onMounted(()=>{console.log(`${n} ${r} mounted`)});i("willUpdateProps")&&o.hooks.onWillUpdateProps(o=>{console.log(`${n} ${r} willUpdateProps, nextprops=${l(o)}`)});i("willPatch")&&o.hooks.onWillPatch(()=>{console.log(`${n} ${r} willPatch`)});i("patched")&&o.hooks.onPatched(()=>{console.log(`${n} ${r} patched`)});i("willUnmount")&&o.hooks.onWillUnmount(()=>{console.log(`${n} ${r} willUnmount`)});const u=e.__render.bind(e);e.__render=function(...o){console.log(`${n} ${r} rendering template`),u(...o)};const d=e.render.bind(e);e.render=function(...o){return console.log(`${n} ${r} render`),d(...o)};const p=e.mount.bind(e);e.mount=function(...o){return console.log(`${n} ${r} mount`),p(...o)}}(s,c,(r=e).id)}})}}),t.logScheduler){let t=o.Component.scheduler.start,e=o.Component.scheduler.stop;o.Component.scheduler.start=function(){console.log(`${n} scheduler: start running tasks queue`),t.call(this)},o.Component.scheduler.stop=function(){console.log(`${n} scheduler: stop running tasks queue`),e.call(this)}}if(t.logStore){let t=o.Store.prototype.dispatch;o.Store.prototype.dispatch=function(o,...e){return console.log(`${n} store: action '${o}' dispatched. Payload: '${l(e)}'`),t.call(this,o,...e)}}}
debugOwl({
function debugOwl(t,n){let e,o="[OWL_DEBUG]";function s(t){let n=JSON.stringify(t||{});return n.length>200&&(n=n.slice(0,200)+"..."),n}if(Object.defineProperty(t.Component,"current",{get:()=>e,set(i){e=i;const r=i.constructor.name;if(n.componentBlackList&&n.componentBlackList.test(r))return;if(n.componentWhiteList&&!n.componentWhiteList.test(r))return;let l;Object.defineProperty(e,"__owl__",{get:()=>l,set(e){!function(e,i,r){let l=`${i}<id=${r}>`,c=t=>console.log(`${o} ${l} ${t}`),u=t=>(!n.methodBlackList||!n.methodBlackList.includes(t))&&!(n.methodWhiteList&&!n.methodWhiteList.includes(t));u("constructor")&&c(`constructor, props=${s(e.props)}`);u("willStart")&&t.hooks.onWillStart(()=>{c("willStart")});u("mounted")&&t.hooks.onMounted(()=>{c("mounted")});u("willUpdateProps")&&t.hooks.onWillUpdateProps(t=>{c(`willUpdateProps, nextprops=${s(t)}`)});u("willPatch")&&t.hooks.onWillPatch(()=>{c("willPatch")});u("patched")&&t.hooks.onPatched(()=>{c("patched")});u("willUnmount")&&t.hooks.onWillUnmount(()=>{c("willUnmount")});const d=e.__render.bind(e);e.__render=function(...t){c("rendering template"),d(...t)};const h=e.render.bind(e);e.render=function(...t){const n=e.__owl__;let o="render";return n.isMounted||n.currentFiber||(o+=" (warning: component is not mounted, this render has no effect)"),c(o),h(...t)};const p=e.mount.bind(e);e.mount=function(...t){return c("mount"),p(...t)}}(i,r,(l=e).id)}})}}),n.logScheduler){let n=t.Component.scheduler.start,e=t.Component.scheduler.stop;t.Component.scheduler.start=function(){this.isRunning||console.log(`${o} scheduler: start running tasks queue`),n.call(this)},t.Component.scheduler.stop=function(){this.isRunning&&console.log(`${o} scheduler: stop running tasks queue`),e.call(this)}}if(n.logStore){let n=t.Store.prototype.dispatch;t.Store.prototype.dispatch=function(t,...e){return console.log(`${o} store: action '${t}' dispatched. Payload: '${s(e)}'`),n.call(this,t,...e)}}}
debugOwl(owl, {
// componentBlackList: /App/, // regexp
// componentWhiteList: /SomeComponent/, // regexp
// methodBlackList: ["mounted"], // list of method names
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "owl-framework",
"version": "1.0.0-beta1",
"version": "1.0.0-beta2",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"engines": {
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.0.0-beta1
- Current version: 1.0.0-beta2
- Status: mostly stable
This roadmap is only an attempt at predicting Owl's future. Everything may
+34 -13
View File
@@ -36,6 +36,10 @@ export interface Env {
[key: string]: any;
}
interface MountOptions {
position?: "first-child" | "last-child" | "self";
}
/**
* This is mostly an internal detail of implementation. The Meta interface is
* useful to typecheck and describe the internal keys used by Owl to manage the
@@ -66,10 +70,9 @@ interface Internal<T extends Env, Props> {
parentLastFiberId: number;
// when a rendering is initiated by a parent, it may set variables in 'scope'
// and 'vars' (typically when the component is rendered in a slot). We need to
// (typically when the component is rendered in a slot). We need to
// store that information in case the component would be re-rendered later on.
scope: any;
vars: any;
boundHandlers: { [key: number]: any };
observer: Observer | null;
@@ -102,6 +105,7 @@ export class Component<T extends Env, Props extends {}> {
static env: any = {};
// expose scheduler s.t. it can be mocked for testing purposes
static scheduler: Scheduler = scheduler;
__target: HTMLElement | undefined;
/**
* The `el` is the root element of the component. Note that it could be null:
@@ -193,8 +197,7 @@ export class Component<T extends Env, Props extends {}> {
renderFn: qweb.render.bind(qweb, template),
classObj: null,
refs: null,
scope: null,
vars: null
scope: null
};
}
@@ -291,7 +294,8 @@ export class Component<T extends Env, Props extends {}> {
*
* Note that a component can be mounted an unmounted several times
*/
async mount(target: HTMLElement | DocumentFragment): Promise<void> {
async mount(target: HTMLElement | DocumentFragment, options: MountOptions = {}): Promise<void> {
const position = options.position || "last-child";
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
return Promise.resolve();
@@ -301,7 +305,16 @@ export class Component<T extends Env, Props extends {}> {
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message);
}
const fiber = new Fiber(null, this, false, target);
let inserter =
position === "last-child"
? el => target.appendChild(el)
: position === "first-child"
? el => target.prepend(el)
: el => {};
if (position === "self") {
this.__target = target as HTMLElement;
}
const fiber = new Fiber(null, this, false, inserter);
fiber.shouldPatch = false;
if (!__owl__.vnode) {
this.__prepareAndRender(fiber, () => {});
@@ -495,9 +508,8 @@ export class Component<T extends Env, Props extends {}> {
* The __updateProps method is called by the t-component directive whenever
* it updates a component (so, when the parent template is rerendered).
*/
async __updateProps(nextProps: Props, parentFiber: Fiber, scope: any, vars: any): Promise<void> {
async __updateProps(nextProps: Props, parentFiber: Fiber, scope: any): Promise<void> {
this.__owl__.scope = scope;
this.__owl__.vars = vars;
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const __owl__ = this.__owl__;
@@ -535,18 +547,27 @@ export class Component<T extends Env, Props extends {}> {
*/
__patch(vnode: VNode) {
const __owl__ = this.__owl__;
const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode);
if (this.__target) {
if (this.__target.tagName.toLowerCase() !== vnode.sel) {
throw new Error(
`Cannot attach '${this.constructor.name}' to target node (not same tag name)`
);
}
__owl__.vnode = patch(this.__target, vnode);
delete this.__target;
} else {
const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode);
}
}
/**
* The __prepare method is only called by the t-component directive, when a
* subcomponent is created. It gets its scope and vars, if any, from the
* subcomponent is created. It gets its scope, if any, from the
* parent template.
*/
__prepare(parentFiber: Fiber, scope: any, vars: any, cb: CallableFunction): Fiber {
__prepare(parentFiber: Fiber, scope: any, cb: CallableFunction): Fiber {
this.__owl__.scope = scope;
this.__owl__.vars = vars;
const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
fiber.shouldPatch = false;
if (!parentFiber.child) {
+10 -27
View File
@@ -190,10 +190,10 @@ QWeb.addDirective({
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//COMPONENT");
ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true;
ctx.rootContext.shouldDefineScope = true;
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
// t-on- events and t-transition
@@ -285,7 +285,7 @@ QWeb.addDirective({
}
let eventsCode = events
.map(function([eventName, mods, handlerValue, extraArgs]) {
let params = "owner";
let params = "context";
if (extraArgs) {
if (ctx.loopNumber) {
let argId = ctx.generateID();
@@ -293,20 +293,20 @@ QWeb.addDirective({
// be set asynchronously later when the widget is ready, and the
// context might be different.
ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`);
params = `owner, arg${argId}`;
params = `context, arg${argId}`;
} else {
params = `owner, ${ctx.formatExpression(extraArgs)}`;
params = `context, ${ctx.formatExpression(extraArgs)}`;
}
}
let handler = `function (e) {`;
let handler = `function (e) {if(!context.__owl__.isMounted){return}`;
handler += mods
.map(function(mod) {
return T_COMPONENT_MODS_CODE[mod];
})
.join("");
if (handlerValue) {
handler += `const fn = owner['${handlerValue}'];`;
handler += `if (fn) { fn.call(${params}, e); } else { owner.${handlerValue}; }`;
handler += `const fn = context['${handlerValue}'];`;
handler += `if (fn) { fn.call(${params}, e); } else { context.${handlerValue}; }`;
}
handler += `}`;
return `vn.elm.addEventListener('${eventName}', ${handler});`;
@@ -348,25 +348,9 @@ QWeb.addDirective({
}
// SLOTS
const varDefs: string[] = [];
const hasSlots = node.childNodes.length;
if (hasSlots) {
ctx.rootContext.shouldTrackScope = true;
for (let v of Object.values(ctx.variables)) {
if (v["id"]) {
varDefs.push(v["id"]);
}
}
}
let scopeVars;
if (hasSlots) {
let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
scopeVars = `${scope}, ${vars}`;
} else {
scopeVars = "undefined, undefined";
}
let scope = hasSlots ? `Object.assign(Object.create(context), scope)` : "undefined";
ctx.addIf(`w${componentID}`);
@@ -376,8 +360,7 @@ QWeb.addDirective({
styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
}
ctx.addLine(
`w${componentID}.__updateProps(props${componentID}, extra.fiber${scopeVars &&
", " + scopeVars})${styleCode};`
`w${componentID}.__updateProps(props${componentID}, extra.fiber, ${scope})${styleCode};`
);
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
if (registerCode) {
@@ -439,7 +422,7 @@ QWeb.addDirective({
}
ctx.addLine(
`let fiber = w${componentID}.__prepare(extra.fiber, ${scopeVars}, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
`let fiber = w${componentID}.__prepare(extra.fiber, ${scope}, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
const insertHook = refExpr ? `insert(vn) {${refExpr}},` : "";
+10 -12
View File
@@ -46,10 +46,9 @@ export class Fiber {
// scheduler.
counter: number = 0;
target: HTMLElement | null;
inserter: (el: HTMLElement) => void | null;
scope: any;
vars: any;
component: Component<any, any>;
vnode: VNode | null = null;
@@ -62,14 +61,13 @@ export class Fiber {
error?: Error;
constructor(parent: Fiber | null, component: Component<any, any>, force, target) {
constructor(parent: Fiber | null, component: Component<any, any>, force, inserter) {
this.component = component;
this.force = force;
this.target = target;
this.inserter = inserter;
const __owl__ = component.__owl__;
this.scope = __owl__.scope;
this.vars = __owl__.vars;
this.root = parent ? parent.root : this;
this.parent = parent;
@@ -181,7 +179,7 @@ export class Fiber {
complete() {
let component = this.component;
this.isCompleted = true;
if (!this.target && !component.__owl__.isMounted) {
if (!this.inserter && !component.__owl__.isMounted) {
return;
}
@@ -211,7 +209,7 @@ export class Fiber {
const fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode!);
if (!fiber.shouldPatch && (!fiber.target || i !== 0)) {
if (!fiber.shouldPatch && (!fiber.inserter || i !== 0)) {
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
}
component.__owl__.currentFiber = null;
@@ -219,9 +217,9 @@ export class Fiber {
// insert into the DOM (mount case)
let inDOM = false;
if (this.target) {
this.target.appendChild(this.component.el!);
inDOM = document.body.contains(this.target);
if (this.inserter) {
this.inserter(this.component.el!);
inDOM = document.body.contains(this.component.el);
this.component.env.qweb.trigger("dom-appended");
}
@@ -229,12 +227,12 @@ export class Fiber {
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
if (fiber.shouldPatch && !this.target) {
if (fiber.shouldPatch && !this.inserter) {
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
} else if (this.target ? inDOM : true) {
} else if (this.inserter ? inDOM : true) {
component.__callMounted();
}
}
-1
View File
@@ -20,7 +20,6 @@
export class Observer {
rev: number = 1;
allowMutations: boolean = true;
dirty: boolean = false;
weakMap: WeakMap<any, any> = new WeakMap();
notifyCB() {}
+12 -1
View File
@@ -59,6 +59,16 @@ export class Portal extends Component<any, any> {
}
});
}
/**
* Override to revert back to a classic Component's structure
*
* @override
*/
__callWillUnmount() {
super.__callWillUnmount();
this.el!.appendChild(this.portal!.elm!);
this.doTargetLookUp = true;
}
/**
* At each DOM change, we must ensure that the portal contains exactly one
* child
@@ -126,7 +136,8 @@ export class Portal extends Component<any, any> {
}
}
this.__checkVNodeStructure(vnode);
const shouldDeploy = !this.portal && !this.doTargetLookUp;
const shouldDeploy =
(!this.portal || this.el!.contains(this.portal.elm!)) && !this.doTargetLookUp;
if (!this.doTargetLookUp && !shouldDeploy) {
// Only on pure patching, provided the
+130 -108
View File
@@ -19,38 +19,40 @@ import { htmlToVDOM } from "../vdom/html_to_vdom";
//------------------------------------------------------------------------------
// t-esc and t-raw
//------------------------------------------------------------------------------
QWeb.utils.getFragment = function(str: string): DocumentFragment {
const temp = document.createElement("template");
temp.innerHTML = str;
return temp.content;
};
QWeb.utils.htmlToVDOM = htmlToVDOM;
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: CompilationContext) {
ctx.rootContext.shouldDefineScope = true;
if (value === "0") {
const caller = ctx.getCaller();
if (caller) {
qweb._compileNode(caller, ctx.getInliningContext());
return;
}
}
if (value.xml instanceof NodeList && !value.id) {
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
if (ctx.parentNode) {
// the 'zero' magical symbol is where we can find the result of the rendering
// of the body of the t-call.
ctx.rootContext.shouldDefineUtils = true;
const zeroArgs = ctx.escaping
? `{text: utils.vDomToString(scope[utils.zero])}`
: `...scope[utils.zero]`;
ctx.addLine(`c${ctx.parentNode}.push(${zeroArgs});`);
}
return;
}
let exprID: string;
if (typeof value === "string") {
exprID = `_${ctx.generateID()}`;
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
} else {
exprID = value.id;
exprID = `scope.${value.id}`;
}
ctx.addIf(`${exprID} || ${exprID} === 0`);
if (ctx.escaping) {
let protectID;
if (value.hasBody) {
protectID = ctx.startProtectScope();
ctx.addLine(
`${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};`
);
}
if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
} else if (ctx.parentNode) {
@@ -64,21 +66,25 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
ctx.addLine(`result = vn${nodeID}`);
}
}
if (value.hasBody) {
ctx.stopProtectScope(protectID);
}
} else {
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
if (value.hasBody) {
ctx.addLine(
`const vnodeArray = ${exprID} instanceof utils.VDomArray ? ${exprID} : utils.htmlToVDOM(${exprID});`
);
ctx.addLine(`c${ctx.parentNode}.push(...vnodeArray);`);
} else {
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
}
}
if (node.childNodes.length) {
ctx.addElse();
qweb._compileChildren(node, ctx);
}
if (value.xml instanceof NodeList && value.id) {
ctx.addElse();
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
}
}
ctx.closeIf();
}
@@ -109,24 +115,46 @@ QWeb.addDirective({
name: "set",
extraNames: ["value"],
priority: 60,
atNodeEncounter({ node, ctx }): boolean {
atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldDefineScope = true;
const variable = node.getAttribute("t-set")!;
let value = node.getAttribute("t-value")!;
ctx.variables[variable] = ctx.variables[variable] || {};
let qwebvar = ctx.variables[variable];
const hasBody = node.hasChildNodes();
qwebvar.id = variable;
qwebvar.expr = `scope.${variable}`;
if (value) {
const formattedValue = ctx.formatExpression(value);
if (ctx.variables.hasOwnProperty(variable) && qwebvar.id) {
ctx.addLine(`${qwebvar.id} = ${formattedValue}`);
} else {
const varName = `_${ctx.generateID()}`;
ctx.addLine(`var ${varName} = ${formattedValue};`);
qwebvar.id = varName;
qwebvar.expr = formattedValue;
ctx.addLine(`${qwebvar.expr} = ${formattedValue};`);
qwebvar.value = formattedValue;
}
if (hasBody) {
ctx.rootContext.shouldDefineUtils = true;
if (value) {
ctx.addIf(`!(${qwebvar.expr})`);
}
const tempParentNodeID = ctx.generateID();
const _parentNode = ctx.parentNode;
ctx.parentNode = tempParentNodeID;
ctx.addLine(`const c${tempParentNodeID} = new utils.VDomArray();`);
const nodeCopy = node.cloneNode(true) as Element;
for (let attr of ["t-set", "t-value", "t-if", "t-else", "t-elif"]) {
nodeCopy.removeAttribute(attr);
}
qweb._compileNode(nodeCopy, ctx);
ctx.addLine(`${qwebvar.expr} = c${tempParentNodeID}`);
qwebvar.value = `c${tempParentNodeID}`;
qwebvar.hasBody = true;
ctx.parentNode = _parentNode;
if (value) {
ctx.closeIf();
}
} else {
qwebvar.xml = node.childNodes;
}
return true;
}
@@ -140,7 +168,7 @@ QWeb.addDirective({
priority: 20,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-if")!);
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : cond.id!);
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id!}`);
return false;
},
finalize({ ctx }) {
@@ -153,7 +181,9 @@ QWeb.addDirective({
priority: 30,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-elif")!);
ctx.addLine(`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : cond.id}) {`);
ctx.addLine(
`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id}`}) {`
);
ctx.indent();
return false;
},
@@ -182,6 +212,9 @@ QWeb.addDirective({
name: "call",
priority: 50,
atNodeEncounter({ node, qweb, ctx }): boolean {
// Step 1: sanity checks
// ------------------------------------------------
ctx.rootContext.shouldDefineScope = true;
if (node.nodeName !== "t") {
throw new Error("Invalid tag for t-call directive (should be 't')");
}
@@ -190,77 +223,63 @@ QWeb.addDirective({
if (!nodeTemplate) {
throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
}
const nodeCopy = node.cloneNode(true) as Element;
nodeCopy.removeAttribute("t-call");
// extract variables from nodecopy
const tempCtx = new CompilationContext();
tempCtx.allowMultipleRoots = true;
qweb._compileNode(nodeCopy, tempCtx);
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
const templateMap = Object.create(ctx.templates);
// open new scope, if necessary
const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
// compile sub template
let subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
subCtx = subCtx.subContext("templates", templateMap);
if (templateMap[subTemplate]) {
// OUCH, IT IS A RECURSIVE TEMPLATE SITUATION...
// This is a tricky situation... We obviously cannot inline the compiled
// template. So, what we need to do is to compile it, and make sure we
// properly transfer everything from the current scope to the sub template.
ctx.rootContext.shouldTrackScope = true;
ctx.rootContext.shouldDefineOwner = true;
let subTemplateName;
if (ctx.hasParentWidget) {
subTemplateName = ctx.templateName;
} else {
subTemplateName = `__${ctx.generateID()}`;
subCtx.variables = {};
let id = 0;
for (let v in vars) {
subCtx.variables[v] = vars[v];
(vars[v] as any).id = `_v${id++}`;
}
const subTemplateFn = qweb._compile(subTemplateName, nodeTemplate.elem, subCtx);
qweb.recursiveFns[subTemplateName] = subTemplateFn;
}
let varCode = `{}`;
if (Object.keys(vars).length) {
let id = 0;
const content = Object.values(vars)
.map((v: any) => `_v${id++}: ${v.expr}`)
.join(",");
varCode = `{${content}}`;
}
ctx.addLine(
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`
);
return true;
// Step 2: compile target template in sub templates
// ------------------------------------------------
if (!qweb.subTemplates[subTemplate]) {
qweb.subTemplates[subTemplate] = true;
const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx);
qweb.subTemplates[subTemplate] = subTemplateFn;
}
templateMap[subTemplate] = true;
if (hasNewVariables) {
ctx.addLine("{");
// Step 3: compile t-call body if necessary
// ------------------------------------------------
let hasBody = node.hasChildNodes();
let protectID;
if (hasBody) {
// we add a sub scope to protect the ambient scope
ctx.addLine(`{`);
ctx.indent();
// add new variables, if any
for (let key in tempCtx.variables) {
const v = tempCtx.variables[key];
if (v.expr) {
ctx.addLine(`let ${v.id} = ${v.expr};`);
}
// todo: handle XML variables...
}
}
qweb._compileNode(nodeTemplate.elem, subCtx);
// close new scope
if (hasNewVariables) {
protectID = ctx.startProtectScope();
const nodeCopy = node.cloneNode(true) as Element;
nodeCopy.removeAttribute("t-call");
const parentNode = ctx.parentNode;
ctx.parentNode = "__0";
// this local scope is intended to trap c__0
ctx.addLine(`{`);
ctx.indent();
ctx.addLine("let c__0 = [];");
qweb._compileNode(nodeCopy, ctx);
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine("scope[utils.zero] = c__0;");
ctx.parentNode = parentNode;
ctx.dedent();
ctx.addLine("}");
ctx.addLine(`}`);
}
// Step 4: add the appropriate function call to current component
// ------------------------------------------------
const callingScope = hasBody ? "scope" : "Object.assign(Object.create(context), scope)";
if (ctx.parentNode) {
ctx.addLine(
`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, Object.assign({}, extra, {parentNode: c${ctx.parentNode}}));`
);
} else {
// this is a t-call with no parentnode, we need to extract the result
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`result = []`);
ctx.addLine(
`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, Object.assign({}, extra, {parentNode: result}));`
);
ctx.addLine(`result = result[0]`);
}
// Step 5: restore previous scope
// ------------------------------------------------
if (hasBody) {
ctx.stopProtectScope(protectID);
ctx.dedent();
ctx.addLine(`}`);
}
return true;
@@ -275,7 +294,7 @@ QWeb.addDirective({
extraNames: ["as"],
priority: 10,
atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldProtectContext = true;
ctx.rootContext.shouldDefineScope = true;
ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1);
const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!;
@@ -290,14 +309,16 @@ QWeb.addDirective({
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf();
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
let varsID = ctx.startProtectScope();
const loopVar = `i${ctx.loopNumber}`;
ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
ctx.indent();
ctx.addToScope(name + "_first", `${loopVar} === 0`);
ctx.addToScope(name + "_last", `${loopVar} === _length${keysID} - 1`);
ctx.addToScope(name + "_index", loopVar);
ctx.addToScope(name, `_${keysID}[${loopVar}]`);
ctx.addToScope(name + "_value", `_${valuesID}[${loopVar}]`);
ctx.addLine(`scope.${name}_first = ${loopVar} === 0`);
ctx.addLine(`scope.${name}_last = ${loopVar} === _length${keysID} - 1`);
ctx.addLine(`scope.${name}_index = ${loopVar}`);
ctx.addLine(`scope.${name} = _${keysID}[${loopVar}]`);
ctx.addLine(`scope.${name}_value = _${valuesID}[${loopVar}]`);
const nodeCopy = <Element>node.cloneNode(true);
let shouldWarn =
!nodeCopy.hasAttribute("t-key") &&
@@ -314,6 +335,7 @@ QWeb.addDirective({
qweb._compileNode(nodeCopy, ctx);
ctx.dedent();
ctx.addLine("}");
ctx.stopProtectScope(varsID);
return true;
}
});
+17 -58
View File
@@ -10,35 +10,27 @@ export class CompilationContext {
code: string[] = [];
variables: { [key: string]: QWebVar } = {};
escaping: boolean = false;
parentNode: number | null = null;
parentNode: number | null | string = null;
parentTextNode: number | null = null;
rootNode: number | null = null;
indentLevel: number = 0;
rootContext: CompilationContext;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
shouldDefineParent: boolean = false;
shouldDefineScope: boolean = false;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldDefineRefs: boolean = false;
shouldDefineResult: boolean = true;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
loopNumber: number = 0;
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false;
scopeVars: any[] = [];
currentKey: string = "";
templates: { [key: string]: boolean } = {};
callingLevel: number = 0;
inliningLevel: number = 0;
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
this.templates[this.templateName] = true;
this.addLine("var h = this.h;");
}
@@ -71,30 +63,16 @@ export class CompilationContext {
}
generateCode(): string[] {
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
if (shouldTrackScope) {
// add some vars to scope if needed
for (let scopeVar of this.scopeVars.reverse()) {
let { index, key, indent } = scopeVar;
const prefix = new Array(indent + 2).join(" ");
this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
}
this.code.unshift(" const scope = Object.create(null);");
}
if (this.shouldProtectContext) {
this.code.unshift(" context = Object.create(context);");
}
if (this.shouldDefineResult) {
this.code.unshift(" let result;");
}
if (this.shouldDefineScope) {
this.code.unshift(" let scope = Object.create(context);");
}
if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
}
if (this.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
this.code.unshift(" let owner = context;");
}
if (this.shouldDefineParent) {
if (this.hasParentWidget) {
this.code.unshift(" let parent = extra.parent;");
@@ -131,10 +109,6 @@ export class CompilationContext {
subContext(key: keyof CompilationContext, value: any): CompilationContext {
const newContext = Object.create(this);
newContext[key] = value;
if (key === "caller") {
newContext.callingLevel++;
newContext.inliningLevel++;
}
return newContext;
}
@@ -152,11 +126,6 @@ export class CompilationContext {
return this.code.length - 1;
}
addToScope(key: string, expr: string) {
const index = this.addLine(`context.${key} = ${expr};`);
this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel });
}
addIf(condition: string) {
this.addLine(`if (${condition}) {`);
this.indent();
@@ -172,27 +141,6 @@ export class CompilationContext {
this.dedent();
this.addLine("}");
}
/**
* Recursively (inverse) fetches the `caller` of a context
* Useful to determine to which t-call a t-raw="0" refers
*/
getCaller(targetLevel?: number): Element | null {
if (targetLevel === undefined) {
targetLevel = this.inliningLevel;
}
if (targetLevel === this.callingLevel) {
return this.caller || null;
}
const proto = (this as any).__proto__;
return proto ? proto.getCaller(targetLevel) : null;
}
/**
* Marks the context with the current recursive level
* in which we are for inlining archs (t-raw="0")
*/
getInliningContext(): CompilationContext {
return this.subContext("inliningLevel", this.inliningLevel - 1);
}
getValue(val: any): QWebVar | string {
return val in this.variables ? this.getValue(this.variables[val]) : val;
@@ -205,6 +153,7 @@ export class CompilationContext {
* - replace already defined variables by their internal name
*/
formatExpression(expr: string): string {
this.rootContext.shouldDefineScope = true;
return compileExpr(expr, this.variables);
}
@@ -225,4 +174,14 @@ export class CompilationContext {
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`";
}
startProtectScope(): number {
const protectID = this.generateID();
this.rootContext.shouldDefineScope = true;
this.addLine(`const _origScope${protectID} = scope;`);
this.addLine(`scope = Object.assign(Object.create(context), scope);`);
return protectID;
}
stopProtectScope(protectID: number) {
this.addLine(`scope = _origScope${protectID};`);
}
}
+37 -17
View File
@@ -39,9 +39,10 @@ const WORD_REPLACEMENT = {
};
export interface QWebVar {
id?: string;
expr?: string;
xml?: NodeList;
id: string; // foo
expr: string; // scope.foo (local variables => only foo)
value?: string; // 1 + 3
hasBody?: boolean;
}
//------------------------------------------------------------------------------
@@ -63,6 +64,7 @@ type TKind =
interface Token {
type: TKind;
value: string;
originalValue?: string;
size?: number;
}
@@ -79,7 +81,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
// note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ".split(",");
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>".split(",");
type Tokenizer = (expr: string) => Token | false;
@@ -227,35 +229,53 @@ export function tokenize(expr: string): Token[] {
* - unless the previous token is a dot (in that case, this is a property: `a.b`)
* - or if the previous token is a left brace or a comma, and the next token is
* a colon (in that case, this is an object key: `{a: b}`)
*
* Some specific code is also required to support arrow functions. If we detect
* the arrow operator, then we add the current (or some previous tokens) token to
* the list of variables so it does not get replaced by a lookup in the context
*/
export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): string {
export function compileExpr(expr: string, scope: { [key: string]: QWebVar }): string {
scope = Object.create(scope);
const tokens = tokenize(expr);
let result = "";
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
let prevToken = tokens[i - 1];
let nextToken = tokens[i + 1];
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
// we need to find if it is a variable
let isVar = true;
let prevToken = tokens[i - 1];
if (prevToken) {
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false;
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
let nextToken = tokens[i + 1];
if (nextToken && nextToken.type === "COLON") {
isVar = false;
}
}
}
if (isVar) {
if (token.value in vars && "id" in vars[token.value]) {
token.value = vars[token.value].id!;
} else {
token.value = `context['${token.value}']`;
}
if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
if (token.type === "RIGHT_PAREN") {
let j = i - 1;
while (j > 0 && tokens[j].type !== "LEFT_PAREN") {
if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) {
tokens[j].value = tokens[j].originalValue!;
scope[tokens[j].value] = { id: tokens[j].value, expr: tokens[j].value };
}
j--;
}
} else {
scope[token.value] = { id: token.value, expr: token.value };
}
}
if (isVar) {
if (token.value in scope && "id" in scope[token.value]) {
token.value = scope[token.value].expr!;
} else {
token.originalValue = token.value;
token.value = `scope['${token.value}']`;
}
}
result += token.value;
}
return result;
return tokens.map(t => t.value).join("");
}
+10 -6
View File
@@ -30,7 +30,6 @@ QWeb.addDirective({
name: "on",
priority: 90,
atNodeCreation({ ctx, fullName, value, nodeID }) {
ctx.rootContext.shouldDefineOwner = true;
const [eventName, ...mods] = fullName.slice(5).split(".");
if (!eventName) {
throw new Error("Missing event name with t-on directive");
@@ -40,8 +39,8 @@ QWeb.addDirective({
extraArgs = args.slice(1, -1);
return "";
});
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
let handler = `function (e) {`;
let params = extraArgs ? `context, ${ctx.formatExpression(extraArgs)}` : "context";
let handler = `function (e) {if (!context.__owl__.isMounted){return}`;
handler += mods
.map(function(mod) {
return MODS_CODE[mod];
@@ -195,9 +194,8 @@ QWeb.addDirective({
QWeb.addDirective({
name: "slot",
priority: 80,
atNodeEncounter({ ctx, value }): boolean {
atNodeEncounter({ ctx, value, node, qweb }): boolean {
const slotKey = ctx.generateID();
ctx.rootContext.shouldDefineOwner = true;
ctx.addLine(
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
);
@@ -211,11 +209,17 @@ QWeb.addDirective({
ctx.addLine(`result = {}`);
}
ctx.addLine(
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: extra.parent || owner}));`
`slot${slotKey}.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: ${parentNode}, parent: extra.parent || context}));`
);
if (!ctx.parentNode) {
ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`);
}
if (node.hasChildNodes()) {
ctx.addElse();
const nodeCopy = <Element>node.cloneNode(true);
nodeCopy.removeAttribute("t-slot");
qweb._compileNode(nodeCopy, ctx);
}
ctx.closeIf();
return true;
}
+28 -23
View File
@@ -87,6 +87,7 @@ interface Utils {
}
const UTILS: Utils = {
zero: Symbol("zero"),
toObj(expr) {
if (typeof expr === "string") {
expr = expr.trim();
@@ -105,6 +106,20 @@ const UTILS: Utils = {
shallowEqual,
addNameSpace(vnode) {
addNS(vnode.data, vnode.children, vnode.sel);
},
VDomArray: class VDomArray extends Array {},
vDomToString: function(vdom: VNode[]): string {
return vdom
.map(vnode => {
if (vnode.sel) {
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
} else {
return vnode.text;
}
})
.join();
}
};
@@ -171,7 +186,7 @@ export class QWeb extends EventBus {
// recursiveTemplates contains sub templates called with t-call, but which
// ends up in recursive situations. This is very similar to the slot situation,
// as in we need to propagate the scope.
recursiveFns = {};
subTemplates = {};
isUpdating: boolean = false;
translateFn?: QWebConfig["translateFn"];
@@ -360,23 +375,12 @@ export class QWeb extends EventBus {
ctx.shouldDefineResult = false;
}
if (parentContext) {
ctx.templates = Object.create(parentContext.templates);
ctx.variables = Object.create(parentContext.variables);
ctx.parentNode = parentContext.parentNode || ctx.generateID();
ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true;
ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
for (let v in parentContext.variables) {
let variable = <any>parentContext.variables[v];
if (variable.id) {
ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`);
}
}
}
if (parentContext) {
ctx.addLine(" Object.assign(context, extra.fiber.scope);");
}
this._compileNode(elem, ctx);
@@ -392,12 +396,13 @@ export class QWeb extends EventBus {
}
let code = ctx.generateCode();
const templateName = ctx.templateName.replace(/`/g, "'").slice(0, 200);
code.unshift(` // Template name: "${templateName}"`);
let template;
try {
template = new Function("context", "extra", code.join("\n")) as CompiledTemplate;
template = new Function("context, extra", code.join("\n")) as CompiledTemplate;
} catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(code.join("\n"));
console.groupEnd();
@@ -453,7 +458,6 @@ export class QWeb extends EventBus {
}
return;
}
if (ctx !== ctx.rootContext) {
ctx = ctx.subContext("currentKey", ctx.currentKey);
}
@@ -650,13 +654,14 @@ export class QWeb extends EventBus {
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
const attID = ctx.generateID();
if (name === "class") {
let classDef = value
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
if ((value = value.trim())) {
let classDef = value
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
} else {
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
@@ -672,7 +677,7 @@ export class QWeb extends EventBus {
if (name.startsWith("t-att-")) {
let attName = name.slice(6);
const v = ctx.getValue(value);
let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : v.id;
let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : `scope.${v.id}`;
if (attName === "class") {
ctx.rootContext.shouldDefineUtils = true;
+1 -1
View File
@@ -219,7 +219,7 @@ function updateClass(oldVnode: VNode, vnode: VNode): void {
elm = vnode.elm as Element;
for (name in oldClass) {
if (!klass[name]) {
if (name && !klass[name]) {
elm.classList.remove(name);
}
}
+24 -19
View File
@@ -1,12 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`animations t-transition combined with component 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"Parent\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let scope = Object.create(context);
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
@@ -19,18 +20,18 @@ exports[`animations t-transition combined with component 1`] = `
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined, undefined);
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| context['Child'];
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k3] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy();
};
@@ -45,16 +46,17 @@ exports[`animations t-transition combined with component 1`] = `
`;
exports[`animations t-transition combined with t-component and t-if 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"Parent\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let scope = Object.create(context);
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].display) {
if (scope['state'].display) {
//COMPONENT
let k3 = \`__4__\`;
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
@@ -64,18 +66,18 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined, undefined);
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| context['Child'];
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k3] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy();
};
@@ -91,16 +93,17 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
`;
exports[`animations t-transition combined with t-component, remove and re-add before transitionend 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let scope = Object.create(context);
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].flag) {
if (scope['state'].flag) {
//COMPONENT
let k3 = \`__4__\`;
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
@@ -110,18 +113,18 @@ exports[`animations t-transition combined with t-component, remove and re-add be
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined, undefined);
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| context['Child'];
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k3] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy();
};
@@ -137,8 +140,9 @@ exports[`animations t-transition combined with t-component, remove and re-add be
`;
exports[`animations t-transition with no delay/duration 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
var h = this.h;
let c1 = [], p1 = {key:1};
@@ -157,8 +161,9 @@ exports[`animations t-transition with no delay/duration 1`] = `
`;
exports[`animations t-transition, on a simple node (insert) 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
var h = this.h;
let c1 = [], p1 = {key:1};
File diff suppressed because it is too large Load Diff
@@ -1,12 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`props validation props are validated in dev mode (code snapshot) 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"App\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let scope = Object.create(context);
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
@@ -19,16 +20,16 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined, undefined);
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| context['Child'];
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap[k3] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
+429 -1
View File
@@ -164,7 +164,7 @@ describe("basic widget properties", () => {
button.click();
await nextTick();
expect(target.innerHTML).toBe("<div>0<button>Inc</button></div>");
expect(counter.state.counter).toBe(1);
expect(counter.state.counter).toBe(0);
});
test("widget style and classname", async () => {
@@ -358,6 +358,74 @@ describe("basic widget properties", () => {
});
});
describe("mount targets", () => {
test("can attach a component to an existing node (if same tagname)", async () => {
class App extends Component<any, any> {
static template = xml`<div>app</div>`;
}
const div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
await app.mount(div, { position: "self" });
expect(fixture.innerHTML).toBe("<div>app</div>");
});
test("cannot attach a component to an existing node (if not same tagname)", async () => {
class App extends Component<any, any> {
static template = xml`<span>app</span>`;
}
const div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
let error;
try {
await app.mount(div, { position: "self" });
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot attach 'App' to target node (not same tag name)");
});
test("can mount a component (with position='first-child')", async () => {
class App extends Component<any, any> {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture, { position: "first-child" });
expect(fixture.innerHTML).toBe("<div>app</div><span></span>");
});
test("can mount a component (with position='last-child')", async () => {
class App extends Component<any, any> {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture, { position: "last-child" });
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
test("default mount option is 'last-child'", async () => {
class App extends Component<any, any> {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
});
describe("lifecycle hooks", () => {
test("willStart hook is called", async () => {
let willstart = false;
@@ -1614,6 +1682,19 @@ describe("class and style attributes with t-component", () => {
expect(fixture.innerHTML).toBe(`<div><div class="c a b"></div></div>`);
});
test("empty class attribute is not added on widget root el", async () => {
class Child extends Component<any, any> {
static template = xml`<span/>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Child class=""/></div>`;
static components = { Child };
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><span></span></div>`);
});
test("t-att-class is properly added/removed on widget root el", async () => {
class Child extends Widget {
static template = xml`<div class="c"/>`;
@@ -2221,6 +2302,100 @@ describe("other directives with t-component", () => {
expect(steps).toEqual(["GrandChild", "Child", "Parent"]);
});
test("t-on on unmounted components", async () => {
const steps: string[] = [];
let child;
class Child extends Component<any, any> {
static template = xml`<div t-on-click="onClick"/>`;
mounted() {
child = this;
}
onClick() {
steps.push("click");
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child/></div>`;
static components = { Child };
state = useState({ flag: true });
}
const parent = new Parent();
await parent.mount(fixture);
let el = child.el;
el.click();
expect(steps).toEqual(["click"]);
parent.unmount();
expect(child.__owl__.isMounted).toBe(false);
el.click();
expect(steps).toEqual(["click"]);
});
test("t-on on destroyed components", async () => {
const steps: string[] = [];
let child;
class Child extends Component<any, any> {
static template = xml`<div t-on-click="onClick"/>`;
mounted() {
child = this;
}
onClick() {
steps.push("click");
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child t-if="state.flag"/></div>`;
static components = { Child };
state = useState({ flag: true });
}
const parent = new Parent();
await parent.mount(fixture);
let el = child.el;
el.click();
expect(steps).toEqual(["click"]);
parent.state.flag = false;
await nextTick();
expect(child.__owl__.isDestroyed).toBe(true);
el.click();
expect(steps).toEqual(["click"]);
});
test("t-on on destroyed components, part 2", async () => {
const steps: string[] = [];
let child;
class GrandChild extends Component<any, any> {
static template = xml`<div>grandchild</div>`;
}
class Child extends Component<any, any> {
static template = xml`<div><GrandChild t-ref="gc" t-on-click="onClick"/></div>`;
static components = { GrandChild };
gc = useRef("gc");
mounted() {
child = this;
}
onClick() {
steps.push("click");
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child t-if="state.flag"/></div>`;
static components = { Child };
state = useState({ flag: true });
}
const parent = new Parent();
await parent.mount(fixture);
let el = child.gc.el;
el.click();
expect(steps).toEqual(["click"]);
parent.state.flag = false;
await nextTick();
expect(child.__owl__.isDestroyed).toBe(true);
el.click();
expect(steps).toEqual(["click"]);
});
test("t-if works with t-component", async () => {
env.qweb.addTemplate("ParentWidget", `<div><t t-component="child" t-if="state.flag"/></div>`);
class Child extends Widget {}
@@ -3857,6 +4032,76 @@ describe("t-slot directive", () => {
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("named slots can define a default content", async () => {
class Dialog extends Component<any, any> {
static template = xml`
<span>
<t t-slot="header">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Dialog/></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>default content</span></div>");
expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot();
});
test("dafault slots can define a default content", async () => {
class Dialog extends Component<any, any> {
static template = xml`
<span>
<t t-slot="default">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Dialog/></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>default content</span></div>");
expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot();
});
test("default content is not rendered if slot is provided", async () => {
class Dialog extends Component<any, any> {
static template = xml`
<span>
<t t-slot="default">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Dialog>hey</Dialog></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>hey</span></div>");
});
test("default content is not rendered if named slot is provided", async () => {
class Dialog extends Component<any, any> {
static template = xml`
<span>
<t t-slot="header">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Dialog><t t-set="header">hey</t></Dialog></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>hey</span></div>");
});
test("slots are rendered with proper context", async () => {
env.qweb.addTemplates(`
<templates>
@@ -4410,6 +4655,189 @@ describe("t-slot directive", () => {
await nextTick(); // wait for changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("<div><span>B0</span><span>B1</span></div>");
});
test("nested slots in same template", async () => {
let child, child2, child3;
class Child extends Widget {
static template = xml`
<span id="c1">
<div>
<t t-slot="default"/>
</div>
</span>`;
constructor(parent, props) {
super(parent, props);
child = this;
}
}
class Child2 extends Widget {
static template = xml`
<span id="c2">
<t t-slot="default"/>
</span>`;
constructor(parent, props) {
super(parent, props);
child2 = this;
}
}
class Child3 extends Widget {
static template = xml`
<span>Child 3</span>`;
constructor(parent, props) {
super(parent, props);
child3 = this;
}
}
class Parent extends Widget {
static components = { Child, Child2, Child3 };
static template = xml`
<span id="parent">
<Child>
<Child2>
<Child3/>
</Child2>
</Child>
</span>`;
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
'<span id="parent"><span id="c1"><div><span id="c2"><span>Child 3</span></span></div></span></span>'
);
expect(child3.__owl__.parent).toStrictEqual(child2);
expect(child2.__owl__.parent).toStrictEqual(child);
expect(child.__owl__.parent).toStrictEqual(widget);
});
test("t-slot nested within another slot", async () => {
let portal, modal, child3;
class Child3 extends Widget {
static template = xml`
<span>Child 3</span>`;
constructor(parent, props) {
super(parent, props);
child3 = this;
}
}
class Modal extends Widget {
static template = xml`
<span id="modal">
<t t-slot="default"/>
</span>`;
constructor(parent, props) {
super(parent, props);
modal = this;
}
}
class Portal extends Widget {
static template = xml`
<span id="portal">
<t t-slot="default"/>
</span>`;
constructor(parent, props) {
super(parent, props);
portal = this;
}
}
class Dialog extends Widget {
static components = { Modal, Portal };
static template = xml`
<span id="c2">
<Modal>
<Portal>
<t t-slot="default"/>
</Portal>
</Modal>
</span>`;
}
class Parent extends Widget {
static components = { Child3, Dialog };
static template = xml`
<span id="c1">
<Dialog>
<Child3/>
</Dialog>
</span>`;
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
'<span id="c1"><span id="c2"><span id="modal"><span id="portal"><span>Child 3</span></span></span></span></span>'
);
expect(child3.__owl__.parent).toStrictEqual(portal);
expect(portal.__owl__.parent).toStrictEqual(modal);
});
test("t-slot supports many instances", async () => {
let child3;
class Child3 extends Widget {
static template = xml`
<span>Child 3</span>`;
constructor(parent, props) {
super(parent, props);
child3 = this;
}
}
class Dialog extends Widget {
static template = xml`
<span id="c2">
<t t-slot="default"/>
</span>`;
}
class Parent extends Widget {
static components = { Child3, Dialog };
static template = xml`
<span id="c1">
<Dialog>
<Child3 val="state.lol"/>
</Dialog>
</span>`;
state = { lol: "k" };
}
const widget = new Parent();
await widget.mount(fixture);
expect(child3.props.val).toBe("k");
const widget_1 = new Parent();
widget_1.state.lol = "m";
await widget_1.mount(fixture);
expect(child3.props.val).toBe("m");
});
test("slots in slots, with vars", async () => {
class B extends Component<any, any> {
static template = xml`<span><t t-slot="default"/></span>`;
}
class A extends Component<any, any> {
static template = xml`
<div>
<B>
<t t-slot="default"/>
</B>
</div>`;
static components = { B };
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<t t-set="test" t-value="state.name"/>
<A>
<p>hey<t t-esc="test"/></p>
</A>
</div>`;
static components = { A };
state = { name: "aaron" };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span><p>heyaaron</p></span></div></div>");
});
});
describe("t-model directive", () => {
+5
View File
@@ -87,6 +87,11 @@ export function renderToDOM(
context: EvalContext = {},
extra?: any
): HTMLElement | Text {
if (!context.__owl__) {
// we add `__owl__` to better simulate a component as context. This is
// particularly important for event handlers added with the `t-on` directive.
context.__owl__ = { isMounted: true };
}
const vnode = qweb.render(template, context, extra);
// we snapshot here the compiled code. This is useful to prevent unwanted code
+62
View File
@@ -548,6 +548,68 @@ describe("Portal: Basic use and DOM placement", () => {
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
});
test("portal manual unmount", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<span>gloria</span>
</Portal>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<span>gloria</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
parent.unmount();
expect(outside.innerHTML).toBe("");
expect(parent.el!.innerHTML).toBe("<portal><span>gloria</span></portal>");
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<span>gloria</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
});
test("portal manual unmount with subcomponent", async () => {
expect.assertions(9);
class Child extends Component<any, any> {
static template = xml`<span>gloria</span>`;
mounted() {
expect(outside.contains(this.el)).toBeTruthy();
}
willUnmount() {
expect(outside.contains(this.el)).toBeTruthy();
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'">
<Child />
</Portal>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<span>gloria</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
parent.unmount();
expect(outside.innerHTML).toBe("");
expect(parent.el!.innerHTML).toBe("<portal><span>gloria</span></portal>");
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<span>gloria</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
});
});
describe("Portal: Events handling", () => {
File diff suppressed because it is too large Load Diff
+160 -5
View File
@@ -46,6 +46,16 @@ describe("static templates", () => {
expect(renderToString(qweb, "test")).toBe("<div>word</div>");
});
test("div with a class attribute", () => {
qweb.addTemplate("test", `<div class="abc">word</div>`);
expect(renderToString(qweb, "test")).toBe(`<div class="abc">word</div>`);
});
test("div with a empty class attribute", () => {
qweb.addTemplate("test", `<div class="">word</div>`);
expect(renderToString(qweb, "test")).toBe(`<div>word</div>`);
});
test("div with a span child node", () => {
qweb.addTemplate("test", "<div><span>word</span></div>");
expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>");
@@ -129,6 +139,17 @@ describe("t-esc", () => {
qweb.addTemplate("test", `<span t-esc="var">nope</span>`);
expect(renderToString(qweb, "test")).toBe("<span>nope</span>");
});
test("t-esc is escaped", () => {
qweb.addTemplate("test", `<div><t t-set="var"><p>escaped</p></t><t t-esc="var"/></div>`);
const domRendered = renderToDOM(qweb, "test");
expect(domRendered.textContent).toBe("<p>escaped</p>");
});
test("t-esc=0 is escaped", () => {
qweb.addTemplate("test", `<span><t t-esc="0"/></span>`);
qweb.addTemplate("testCaller", `<div><t t-call="test"><p>escaped</p></t></div>`);
const domRendered = renderToDOM(qweb, "testCaller") as HTMLElement;
expect(domRendered.querySelector("span")!.textContent).toBe("<p>escaped</p>");
});
});
describe("t-raw", () => {
@@ -268,6 +289,56 @@ describe("t-set", () => {
expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>");
expect(renderToString(qweb, "test", { flag: false })).toBe("<div>0</div>");
});
test("t-set body is evaluated immediately", () => {
qweb.addTemplate(
"test",
`<div>
<t t-set="v1" t-value="'before'"/>
<t t-set="v2">
<span><t t-esc="v1"/></span>
</t>
<t t-set="v1" t-value="'after'"/>
<t t-raw="v2"/>
</div>`
);
expect(renderToString(qweb, "test")).toBe("<div><span>before</span></div>");
});
test("t-set with t-value (falsy) and body", () => {
qweb.addTemplate(
"test",
`<div>
<t t-set="v3" t-value="false"/>
<t t-set="v1" t-value="'before'"/>
<t t-set="v2" t-value="v3">
<span><t t-esc="v1"/></span>
</t>
<t t-set="v1" t-value="'after'"/>
<t t-set="v3" t-value="true"/>
<t t-raw="v2"/>
</div>`);
expect(renderToString(qweb, "test")).toBe("<div><span>before</span></div>");
});
test("t-set with t-value (truthy) and body", () => {
qweb.addTemplate(
"test",
`<div>
<t t-set="v3" t-value="'Truthy'"/>
<t t-set="v1" t-value="'before'"/>
<t t-set="v2" t-value="v3">
<span><t t-esc="v1"/></span>
</t>
<t t-set="v1" t-value="'after'"/>
<t t-set="v3" t-value="false"/>
<t t-raw="v2"/>
</div>`);
expect(renderToString(qweb, "test")).toBe("<div>Truthy</div>");
});
});
describe("t-if", () => {
@@ -432,6 +503,18 @@ describe("attributes", () => {
expect(result).toBe(`<div foo="bar"></div>`);
});
test("dynamic class attribute", () => {
qweb.addTemplate("test", `<div t-att-class="c"/>`);
const result = renderToString(qweb, "test", { c: "abc" });
expect(result).toBe(`<div class="abc"></div>`);
});
test("dynamic empty class attribute", () => {
qweb.addTemplate("test", `<div t-att-class="c"/>`);
const result = renderToString(qweb, "test", { c: "" });
expect(result).toBe(`<div></div>`);
});
test("dynamic attribute with a dash", () => {
qweb.addTemplate("test", `<div t-att-data-action-id="id"/>`);
const result = renderToString(qweb, "test", { id: 32 });
@@ -576,10 +659,19 @@ describe("attributes", () => {
describe("t-call (template calling", () => {
test("basic caller", () => {
qweb.addTemplate("_basic-callee", "<span>ok</span>");
qweb.addTemplate("caller", '<div><t t-call="_basic-callee"/></div>');
const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller")).toBe(expected);
expect(qweb.subTemplates["_basic-callee"].toString()).toMatchSnapshot();
});
test("basic caller, no parent node", () => {
qweb.addTemplate("_basic-callee", "<div>ok</div>");
qweb.addTemplate("caller", '<t t-call="_basic-callee"/>');
const expected = "<div>ok</div>";
expect(renderToString(qweb, "caller")).toBe(expected);
expect(qweb.subTemplates["_basic-callee"].toString()).toMatchSnapshot();
});
test("t-call with t-if", () => {
@@ -587,6 +679,7 @@ describe("t-call (template calling", () => {
qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>');
const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller", { flag: true })).toBe(expected);
expect(qweb.subTemplates["sub"].toString()).toMatchSnapshot();
});
test("t-call not allowed on a non t node", () => {
@@ -671,6 +764,7 @@ describe("t-call (template calling", () => {
`);
const expected = "<div><div><span>hey</span> <span>yay</span></div></div>";
expect(renderToString(qweb, "main")).toBe(expected);
expect(qweb.subTemplates["SubTemplate"].toString()).toMatchSnapshot();
});
test("cascading t-call t-raw='0'", () => {
@@ -720,7 +814,7 @@ describe("t-call (template calling", () => {
`);
const expected = "<div><span>hey</span></div>";
expect(renderToString(qweb, "recursive")).toBe(expected);
const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
const recursiveFn = Object.values(qweb.subTemplates)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot();
});
@@ -748,7 +842,7 @@ describe("t-call (template calling", () => {
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe(
expected
);
const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
const recursiveFn = Object.values(qweb.subTemplates)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot();
});
@@ -775,7 +869,7 @@ describe("t-call (template calling", () => {
const expected =
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>";
expect(renderToString(qweb, "Parent", { root }, { fiber: {} })).toBe(expected);
const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
const recursiveFn = Object.values(qweb.subTemplates)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot();
});
@@ -785,6 +879,51 @@ describe("t-call (template calling", () => {
const expected = "<div><span>desk</span></div>";
expect(trim(renderToString(qweb, "abcd"))).toBe(expected);
});
test("t-call with t-set inside and outside", () => {
qweb.addTemplates(`
<templates>
<div t-name="main">
<t t-foreach="list" t-as="v">
<t t-set="val" t-value="v.val"/>
<t t-call="sub">
<t t-set="val3" t-value="val*3"/>
</t>
</t>
</div>
<t t-name="sub">
<span t-esc="val3"/>
</t>
</templates>
`);
const expected = "<div><span>3</span><span>6</span><span>9</span></div>";
const context = { list: [{ val: 1 }, { val: 2 }, { val: 3 }] };
expect(trim(renderToString(qweb, "main", context))).toBe(expected);
});
test("t-call with t-set inside and outside. 2", () => {
qweb.addTemplates(`
<templates>
<div t-name="main">
<t t-foreach="list" t-as="v">
<t t-set="val" t-value="v.val"/>
<t t-call="sub">
<t t-set="val3" t-value="val*3"/>
</t>
</t>
</div>
<t t-name="sub">
<span t-esc="val3"/>
<t t-esc="w"/>
</t>
<p t-name="wrapper"><t t-set="w" t-value="'fromwrapper'"/><t t-call="main"/></p>
</templates>
`);
const expected =
"<p><div><span>3</span>fromwrapper<span>6</span>fromwrapper<span>9</span>fromwrapper</div></p>";
const context = { list: [{ val: 1 }, { val: 2 }, { val: 3 }] };
expect(trim(renderToString(qweb, "wrapper", context))).toBe(expected);
});
});
describe("foreach", () => {
@@ -853,9 +992,9 @@ describe("foreach", () => {
<t t-foreach="[1]" t-as="item"><t t-esc="item"/></t>
</div>`
);
const context = {};
const context = { __owl__: {} };
renderToString(qweb, "test", context);
expect(Object.keys(context).length).toBe(0);
expect(Object.keys(context)).toEqual(["__owl__"]);
});
test("t-foreach in t-forach", () => {
@@ -1514,6 +1653,22 @@ describe("debugging", () => {
console.log = consoleLog;
});
test("t-debug on sub template", () => {
const consoleLog = console.log;
console.log = jest.fn();
qweb.addTemplates(`
<templates>
<p t-name="sub" t-debug="1">coucou</p>
<div t-name="test">
<t t-call="sub"/>
</div>
</templates>`);
qweb.render("test");
expect(console.log).toHaveBeenCalledTimes(1);
console.log = consoleLog;
});
test("t-log", () => {
const consoleLog = console.log;
console.log = jest.fn();
+36 -29
View File
@@ -92,7 +92,7 @@ describe("expression evaluation", () => {
test("parenthesis", () => {
expect(compileExpr("(1)", {})).toBe("(1)");
expect(compileExpr("a*(1 +3)", {})).toBe("context['a']*(1+3)");
expect(compileExpr("a*(1 +3)", {})).toBe("scope['a']*(1+3)");
});
test("objects and sub objects", () => {
@@ -100,8 +100,8 @@ describe("expression evaluation", () => {
});
test("replacing variables", () => {
expect(compileExpr("a", {})).toBe("context['a']");
expect(compileExpr("a", { a: { id: "_3", expr: "" } })).toBe("_3");
expect(compileExpr("a", {})).toBe("scope['a']");
expect(compileExpr("a", { a: { id: "_3", expr: "scope._3" } })).toBe("scope._3");
});
test("arrays and objects", () => {
@@ -111,56 +111,63 @@ describe("expression evaluation", () => {
});
test("dot operator", () => {
expect(compileExpr("a.b", {})).toBe("context['a'].b");
expect(compileExpr("a.b.c", {})).toBe("context['a'].b.c");
expect(compileExpr("a.b", {})).toBe("scope['a'].b");
expect(compileExpr("a.b.c", {})).toBe("scope['a'].b.c");
});
test("various unary operators", () => {
expect(compileExpr("!flag", {})).toBe("!context['flag']");
expect(compileExpr("!flag", {})).toBe("!scope['flag']");
expect(compileExpr("-3", {})).toBe("-3");
expect(compileExpr("-a", {})).toBe("-context['a']");
expect(compileExpr("typeof a", {})).toBe("typeof context['a']");
expect(compileExpr("-a", {})).toBe("-scope['a']");
expect(compileExpr("typeof a", {})).toBe("typeof scope['a']");
});
test("various binary operators", () => {
expect(compileExpr("color == 'black'", {})).toBe("context['color']=='black'");
expect(compileExpr("a || b", {})).toBe("context['a']||context['b']");
expect(compileExpr("color === 'black'", {})).toBe("context['color']==='black'");
expect(compileExpr("'li_'+item", {})).toBe("'li_'+context['item']");
expect(compileExpr("state.val > 1", {})).toBe("context['state'].val>1");
expect(compileExpr("color == 'black'", {})).toBe("scope['color']=='black'");
expect(compileExpr("a || b", {})).toBe("scope['a']||scope['b']");
expect(compileExpr("color === 'black'", {})).toBe("scope['color']==='black'");
expect(compileExpr("'li_'+item", {})).toBe("'li_'+scope['item']");
expect(compileExpr("state.val > 1", {})).toBe("scope['state'].val>1");
});
test("boolean operations", () => {
expect(compileExpr("a && b", {})).toBe("context['a']&&context['b']");
expect(compileExpr("a && b", {})).toBe("scope['a']&&scope['b']");
});
test("ternary operators", () => {
expect(compileExpr("a ? b: '2'", {})).toBe("context['a']?context['b']:'2'");
expect(compileExpr("a ? b: (c or '2') ", {})).toBe(
"context['a']?context['b']:(context['c']||'2')"
);
expect(compileExpr("a ? b: '2'", {})).toBe("scope['a']?scope['b']:'2'");
expect(compileExpr("a ? b: (c or '2') ", {})).toBe("scope['a']?scope['b']:(scope['c']||'2')");
expect(compileExpr("a ? {test:c}: [1,u]", {})).toBe(
"context['a']?{test:context['c']}:[1,context['u']]"
"scope['a']?{test:scope['c']}:[1,scope['u']]"
);
});
test("word replacement", () => {
expect(compileExpr("a or b", {})).toBe("context['a']||context['b']");
expect(compileExpr("a and b", {})).toBe("context['a']&&context['b']");
expect(compileExpr("a or b", {})).toBe("scope['a']||scope['b']");
expect(compileExpr("a and b", {})).toBe("scope['a']&&scope['b']");
});
test("function calls", () => {
expect(compileExpr("a()", {})).toBe("context['a']()");
expect(compileExpr("a(1)", {})).toBe("context['a'](1)");
expect(compileExpr("a(1,2)", {})).toBe("context['a'](1,2)");
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe("context['a'](1,2,{a:[context['a']]})");
expect(compileExpr("a()", {})).toBe("scope['a']()");
expect(compileExpr("a(1)", {})).toBe("scope['a'](1)");
expect(compileExpr("a(1,2)", {})).toBe("scope['a'](1,2)");
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe("scope['a'](1,2,{a:[scope['a']]})");
expect(compileExpr("'x'.toUpperCase()", {})).toBe("'x'.toUpperCase()");
expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe("'x'.toUpperCase({a:3})");
expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })).toBe(
"'x'.toUpperCase(_v5)"
expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "scope._v5" } })).toBe(
"'x'.toUpperCase(scope._v5)"
);
expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })).toBe(
"'x'.toUpperCase({b:_v5})"
expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "scope._v5" } })).toBe(
"'x'.toUpperCase({b:scope._v5})"
);
});
test("arrow functions", () => {
expect(compileExpr("list.map(e => e.val)", {})).toBe("scope['list'].map(e=>e.val)");
expect(compileExpr("list.map(e => a + e)", {})).toBe("scope['list'].map(e=>scope['a']+e)");
expect(compileExpr("list.map((e) => e)", {})).toBe("scope['list'].map((e)=>e)");
expect(compileExpr("list.map((elem, index) => elem + index)", {})).toBe(
"scope['list'].map((elem,index)=>elem+index)"
);
});
});
+7 -6
View File
@@ -1,20 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Link component can render simple cases 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let utils = this.constructor.utils;
let owner = context;
let scope = Object.create(context);
var h = this.h;
let _5 = utils.toObj({'router-link-active':context['isActive']});
var _6 = context['href'];
let _5 = utils.toObj({'router-link-active':scope['isActive']});
var _6 = scope['href'];
let c7 = [], p7 = {key:7,attrs:{href: _6},class:_5,on:{}};
var vn7 = h('a', p7, c7);
extra.handlers['click' + 7] = extra.handlers['click' + 7] || function (e) {const fn = context['navigate'];if (fn) { fn.call(owner, e); } else { context.navigate; }};
extra.handlers['click' + 7] = extra.handlers['click' + 7] || function (e) {if (!context.__owl__.isMounted){return}const fn = context['navigate'];if (fn) { fn.call(context, e); } else { context.navigate; }};
p7.on['click'] = extra.handlers['click' + 7];
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot8) {
slot8.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c7, vars: extra.vars, parent: extra.parent || owner}));
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context}));
}
return vn7;
}"
@@ -1,37 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RouteComponent can render simple cases 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let scope = Object.create(context);
let result;
var h = this.h;
if (context['routeComponent']) {
const nodeKey5 = context['env'].router.currentRouteName;
if (scope['routeComponent']) {
const nodeKey5 = scope['env'].router.currentRouteName;
//COMPONENT
let k7 = \`__8__\` + nodeKey5;
let w6 = k7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k7]] : false;
let vn9 = {};
result = vn9;
let props6 = Object.assign({}, context['env'].router.currentParams);
let props6 = Object.assign({}, scope['env'].router.currentParams);
if (w6 && w6.__owl__.currentFiber && !w6.__owl__.vnode) {
w6.destroy();
w6 = false;
}
if (w6) {
w6.__updateProps(props6, extra.fiber, undefined, undefined);
w6.__updateProps(props6, extra.fiber, undefined);
let pvnode = w6.__owl__.pvnode;
utils.defineProxy(vn9, pvnode);
} else {
let componentKey6 = \`routeComponent\`;
let W6 = context.constructor.components[componentKey6] || QWeb.components[componentKey6]|| context['routeComponent'];
let W6 = context.constructor.components[componentKey6] || QWeb.components[componentKey6]|| scope['routeComponent'];
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
w6 = new W6(parent, props6);
parent.__owl__.cmap[k7] = w6.__owl__.id;
let fiber = w6.__prepare(extra.fiber, undefined, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w6.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k7, hook: {remove() {},destroy(vn) {w6.destroy();}}});
utils.defineProxy(vn9, pvnode);
w6.__owl__.pvnode = pvnode;
+46
View File
@@ -0,0 +1,46 @@
/**
* We can only make one test per file, since the debug tool modify in place
* the owl object in a way that is difficult to undo.
*/
import { debugOwl } from "../../tools/debug";
import * as owl from "../../src/index";
import { Component, Env } from "../../src/component/component";
import { xml } from "../../src/tags";
import { makeTestFixture, makeTestEnv } from "../helpers";
let fixture: HTMLElement = makeTestFixture();
let env: Env = makeTestEnv();
Component.env = env;
debugOwl(owl, { logScheduler: true });
test("log a specific message for render method calls if component is not mounted", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
class Parent extends Component<any, any> {
static template = xml`<div><t t-esc="state.value"/></div>`;
state = owl.hooks.useState({ value: 1 });
}
const parent = new Parent(null, {});
await parent.mount(fixture);
parent.unmount();
parent.state.value = 2;
expect(steps).toEqual([
"[OWL_DEBUG] Parent<id=1> constructor, props={}",
"[OWL_DEBUG] Parent<id=1> mount",
"[OWL_DEBUG] Parent<id=1> willStart",
"[OWL_DEBUG] scheduler: start running tasks queue",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue",
"[OWL_DEBUG] Parent<id=1> willUnmount",
"[OWL_DEBUG] Parent<id=1> render (warning: component is not mounted, this render has no effect)"
]);
console.log = log;
});
-173
View File
@@ -1,173 +0,0 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
template = "Counter";
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
template = "Message";
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
template = "App";
mounted() {
this.log(
`Benchmarking Owl v${owl._version} (build date: ${
owl._date
})`
);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(data) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.10.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="'log'"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-170
View File
@@ -1,170 +0,0 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(
`Benchmarking Owl v${owl.__info__.version} (build date: ${
owl.__info__.date
})`
);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this.benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(data) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.14.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-161
View File
@@ -1,161 +0,0 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.16.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
-173
View File
@@ -1,173 +0,0 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
template = "Counter";
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
template = "Message";
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
template = "App";
mounted() {
this.log(
`Benchmarking Owl v${owl._version} (build date: ${
owl._date
})`
);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(data) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.8.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl-0.8.0.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="'log'"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
@@ -1,10 +1,11 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
state = useState({ counter: 0 });
increment() {
this.state.counter++;
@@ -15,8 +16,6 @@ class Counter extends owl.Component {
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
@@ -26,13 +25,14 @@ class Message extends owl.Component {
});
}
}
Message.components = { Counter };
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log");
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
@@ -108,10 +108,10 @@ class App extends owl.Component {
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
for (let i = 0; i < messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
messages[i] = msg;
}
});
}
@@ -129,32 +129,25 @@ class App extends owl.Component {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
this.logRef.el.appendChild(div);
this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
this.logRef.el.innerHTML = "";
}
}
App.components = { Message };
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
const templates = await owl.utils.loadFile("templates.xml");
App.env = {
qweb: new owl.QWeb({ templates })
};
const app = new App(env);
const app = new App();
app.mount(document.body);
}
@@ -2,12 +2,11 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.12.0 Benchmark</title>
<title>OWL v1.0.0-beta1 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -12,11 +12,11 @@
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<input type="checkbox" id="multipleflag" t-model="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<input type="checkbox" id="clearFlag" t-model="state.clearAfterFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
+22 -12
View File
@@ -44,6 +44,7 @@
function debugComponent(component, name, id) {
let fullName = `${name}<id=${id}>`;
let log = str => console.log(`${prefix} ${fullName} ${str}`);
let shouldDebug = method => {
if (options.methodBlackList && options.methodBlackList.includes(method)) {
return false;
@@ -54,51 +55,56 @@
return true;
};
if (shouldDebug("constructor")) {
console.log(`${prefix} ${fullName} constructor, props=${toStr(component.props)}`);
log(`constructor, props=${toStr(component.props)}`);
}
if (shouldDebug("willStart")) {
owl.hooks.onWillStart(() => {
console.log(`${prefix} ${fullName} willStart`);
log(`willStart`);
});
}
if (shouldDebug("mounted")) {
owl.hooks.onMounted(() => {
console.log(`${prefix} ${fullName} mounted`);
log(`mounted`);
});
}
if (shouldDebug("willUpdateProps")) {
owl.hooks.onWillUpdateProps(nextProps => {
console.log(`${prefix} ${fullName} willUpdateProps, nextprops=${toStr(nextProps)}`);
log(`willUpdateProps, nextprops=${toStr(nextProps)}`);
});
}
if (shouldDebug("willPatch")) {
owl.hooks.onWillPatch(() => {
console.log(`${prefix} ${fullName} willPatch`);
log(`willPatch`);
});
}
if (shouldDebug("patched")) {
owl.hooks.onPatched(() => {
console.log(`${prefix} ${fullName} patched`);
log(`patched`);
});
}
if (shouldDebug("willUnmount")) {
owl.hooks.onWillUnmount(() => {
console.log(`${prefix} ${fullName} willUnmount`);
log(`willUnmount`);
});
}
const __render = component.__render.bind(component);
component.__render = function(...args) {
console.log(`${prefix} ${fullName} rendering template`);
log(`rendering template`);
__render(...args);
};
const render = component.render.bind(component);
component.render = function(...args) {
console.log(`${prefix} ${fullName} render`);
const __owl__ = component.__owl__;
let msg = `render`;
if (!__owl__.isMounted && !__owl__.currentFiber) {
msg += ` (warning: component is not mounted, this render has no effect)`;
}
log(msg);
return render(...args);
};
const mount = component.mount.bind(component);
component.mount = function(...args) {
console.log(`${prefix} ${fullName} mount`);
log(`mount`);
return mount(...args);
};
}
@@ -107,11 +113,15 @@
let start = owl.Component.scheduler.start;
let stop = owl.Component.scheduler.stop;
owl.Component.scheduler.start = function () {
console.log(`${prefix} scheduler: start running tasks queue`);
if (!this.isRunning) {
console.log(`${prefix} scheduler: start running tasks queue`);
}
start.call(this);
};
owl.Component.scheduler.stop = function () {
console.log(`${prefix} scheduler: stop running tasks queue`);
if (this.isRunning) {
console.log(`${prefix} scheduler: stop running tasks queue`);
}
stop.call(this);
};
}
+1 -5
View File
@@ -28,19 +28,15 @@
<div class="benchmarks">
<ul>
<li><a href="benchmarks/owl-0.7.0">OWL 0.7.0</a></li>
<li><a href="benchmarks/owl-0.8.0">OWL 0.8.0</a></li>
<li><a href="benchmarks/owl-0.9.0">OWL 0.9.0</a></li>
<li><a href="benchmarks/owl-0.10.0">OWL 0.10.0</a></li>
<li><a href="benchmarks/owl-0.11.0">OWL 0.11.0</a></li>
<li><a href="benchmarks/owl-0.12.0">OWL 0.12.0</a></li>
<li><a href="benchmarks/owl-0.13.0">OWL 0.13.0</a></li>
<li><a href="benchmarks/owl-0.14.0">OWL 0.14.0</a></li>
<li><a href="benchmarks/owl-0.15.0">OWL 0.15.0</a></li>
<li><a href="benchmarks/owl-0.16.0">OWL 0.16.0</a></li>
<li><a href="benchmarks/owl-0.17.0">OWL 0.17.0</a></li>
<li><a href="benchmarks/owl-0.18.0">OWL 0.18.0</a></li>
<li><a href="benchmarks/owl-0.21.0">OWL 0.21.0</a></li>
<li><a href="benchmarks/owl-0.24.0">OWL 0.24.0</a></li>
<li><a href="benchmarks/owl-1.0.0-beta1">OWL 1.0.0-beta1</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul>
<ul>