mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0762eeb010 | |||
| bc455be720 | |||
| 9145799ae9 | |||
| ce9c2f8613 | |||
| b890e7ceae | |||
| 124211c18b | |||
| be8c3bbf6f | |||
| e162a6fb6c | |||
| 7ec05ff8cf | |||
| a0bfbf6dd0 | |||
| b67cb71048 | |||
| f5b3ef5cf3 | |||
| ecfccf2448 | |||
| f12d3373c9 | |||
| 286090efca | |||
| 2b4ff7b2fd | |||
| c43178a45b | |||
| 033a431e18 |
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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};`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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)"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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();
|
||||
@@ -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
@@ -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,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
@@ -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,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
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
+1
-2
@@ -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
+2
-2
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user