Compare commits

..

1 Commits

Author SHA1 Message Date
Géry Debongnie 7933328d0a wip 2019-12-02 13:18:50 +01:00
67 changed files with 18907 additions and 11357 deletions
+2 -2
View File
@@ -104,8 +104,8 @@ Submit a PR!
If you want to use a simple `<script>` tag, the last release can be downloaded here: If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.0.0-beta4.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta4/owl.js) - [owl-1.0.0-alpha5.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha5/owl.js)
- [owl-1.0.0-beta4.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta4/owl.min.js) - [owl-1.0.0-alpha5.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha5/owl.min.js)
Some npm scripts are available: Some npm scripts are available:
+14 -14
View File
@@ -50,20 +50,20 @@ and `EventBus` is exported as `owl.core.EventBus`):
``` ```
Component misc Component misc
Context AsyncRoot Context AsyncRoot
QWeb Portal QWeb router
Store router Store Link
useState Link useState RouteComponent
config RouteComponent config Router
mode Router mode tags
core tags core xml
EventBus xml EventBus utils
Observer utils Observer debounce
hooks debounce hooks escape
onWillStart escape onWillStart loadJS
onMounted loadJS onMounted loadFile
onWillUpdateProps loadFile onWillUpdateProps shallowEqual
onWillPatch shallowEqual onWillPatch whenReady
onPatched whenReady onPatched
onWillUnmount onWillUnmount
useContext useContext
useState useState
+7 -41
View File
@@ -234,23 +234,12 @@ component.
We explain here all the public methods of the `Component` class. We explain here all the public methods of the `Component` class.
- **`mount(target, options)`** (async): this is the main way a - **`mount(target)`** (async): this is the main way a
component is added to the DOM: the root component is mounted to a target 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 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 created as well. Most applications will need to call `mount` exactly once, on
the root component. 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 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 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. in the environment, or in the store, or ...) will be taken into account.
@@ -269,25 +258,25 @@ We explain here all the public methods of the `Component` class.
// app is now visible // 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 method can be used. Most applications should not call `unmount`, this is more
useful to the underlying component system. 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 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. 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 Note that the render method is asynchronous, so one cannot observe the updated
DOM in the same stack frame. 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 are updated. It returns a boolean, which indicates if the component should
ignore a props update. If it returns false, then `willUpdateProps` will not ignore a props update. If it returns false, then `willUpdateProps` will not
be called, and no rendering will occur. Its default implementation is to 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 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. 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, and perform all necessary cleanup, such as unmounting the component, its children,
removing the parent/children relationship. This method should almost never be 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 called directly (except maybe on the root component), but should be done by the
@@ -628,20 +617,10 @@ as it will be translated to:
```js ```js
button.addEventListener("click", () => { button.addEventListener("click", () => {
context.state.counter++; component.state.counter++;
}); });
``` ```
Warning: inline expressions are evaluated in the context of the template. This
means that they can access the component methods and properties. But if they set
a key, the inline statement will actually not modify the component, but a key in
a sub scope.
```xml
<button t-on-click="value = 1">Set value to 1 (does not work!!!)</button>
<button t-on-click="state.value = 1">Set state.value to 1 (work as expected)</button>
```
In order to remove the DOM event details from the event handlers (like calls to In order to remove the DOM event details from the event handlers (like calls to
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be `event.preventDefault`) and let them focus on data logic, _modifiers_ can be
specified as additional suffixes of the `t-on` directive. specified as additional suffixes of the `t-on` directive.
@@ -875,19 +854,6 @@ be considered the `default` slot. For example:
</div> </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 ### Dynamic sub components
It is not common, but sometimes we need a dynamic component name. In this case, It is not common, but sometimes we need a dynamic component name. In this case,
@@ -956,7 +922,7 @@ For example, here is how we could implement an `ErrorBoundary` component:
<t t-if="state.error"> <t t-if="state.error">
Error handled Error handled
</t> </t>
<t t-else=""> <t t-else="1">
<t t-slot="default" /> <t t-slot="default" />
</t> </t>
</div> </div>
+1 -1
View File
@@ -57,7 +57,7 @@ class SomeComponent extends Component {
<t t-if=device.isMobile> <t t-if=device.isMobile>
some simplified user interface some simplified user interface
</t> </t>
<t t-else=""> <t t-else="1">
a more advanced user interface a more advanced user interface
</t> </t>
</div>`; </div>`;
-111
View File
@@ -1,116 +1,5 @@
# 🦉 Miscellaneous 🦉 # 🦉 Miscellaneous 🦉
## Content
- [Portal](#portal)
- [AsyncRoot](#asyncroot)
## `Portal`
### Overview
The component `Portal` is meant to be used as a transparent way to 'teleport' a piece
of DOM to the node represented by its sole `target` props.
This component aims at helping the implementation of the needed infrastructure
for modals (as in `bootstrap-modal`).
### Usage
The content it will teleport is defined within the `<Portal>` node and
internally uses the `default` [Slot](component.md#slots).
This slot must contain only **one** node, which in turn can have as many children as necessary.
The element under which the content will be teleported is represented as a selector
by the `target` props which only accepts a string as value.
The `target` props only supports static selector, and is not meant to be passed to `Portal`
as a variable. Namely, `<Portal target="'body'" />` is the intended use.
By contrast, `<Portal target="state.target" />` is not supported.
The component `Portal` has no particular state, rather it is meant to be a slave to its parent,
and ultimately just a way for the parent to teleport a piece of its own DOM elsewhere.
The `Portal`'s root node is always `<portal/>` and is placed where the teleported content
_would have_ been. It is this element that the [teleported events](#expected-behaviors) are re-directed on.
### Example
The canonic use-case is to implement a Dialog, where a Component may choose to break the natural
workflow to help the user put in some data, which it could use later on.
JavaScript:
```js
const { Component } = owl;
const { Portal } = owl.misc;
class TeleportedComponent extends Component {}
class App extends Component {
static components = { Portal, TeleportedComponent };
}
const app = new App();
app.mount(document.body);
```
XML:
```xml
<templates>
<div t-name="TeleportedComponent">
<span>I will move soon enough</span>
</div>
<div t-name="App">
<span>I am like the rest of us</span>
<Portal target="'body'">
<TeleportedComponent />
</Portal>
</div>
</templates>
```
In this example, the `Portal` component will teleport the `TeleportedComponent`'s `div` as a child of the `body`.
`TeleportedComponent` is acting as a Dialog here.
The resulting DOM will look like:
```xml
<body>
<div>
<span>I am like the rest of us</span>
<portal></portal>
</div>
<div>
<span>I will move soon enough</span>
</div>
</body>
```
### Expected Behaviors
The teleported piece is updated as any other `Component`'s DOM and in the same sequence.
Namely the teleported piece will be updated in function of its parents components, and patched as
a normal child.
The [_business_ events](component.md#event-handling) triggered by a child component will be stopped
to not bubble outside of the `target`. They will, on the other hand, be re-directed onto the
`Portal`'s root node and bubble up the DOM as if it were triggered by a regular child component.
Beware that those re-directed events are copies of the original event.
They have:
- The same payload.
- The same `originalComponent` than their original counterpart,
that is the actual Component that triggered it.
- A **different** `target` property than their original counterpart.
The `target` of a re-directed event is necessarily the `Portal`'s root node.
Pure DOM events do not follow this pattern and are free to bubble their natural, unaltered way
up to the `body`.
## `AsyncRoot` ## `AsyncRoot`
When this component is used, a new rendering sub tree is created, such that the When this component is used, a new rendering sub tree is created, such that the
+2 -11
View File
@@ -28,7 +28,7 @@ generate a virtual dom representation of the HTML.
```xml ```xml
<div> <div>
<span t-if="somecondition">Some string</span> <span t-if="somecondition">Some string</span>
<ul t-else=""> <ul t-else="1">
<li t-foreach="messages" t-as="message"> <li t-foreach="messages" t-as="message">
<t t-esc="message"/> <t t-esc="message"/>
</li> </li>
@@ -101,7 +101,7 @@ precisely, the result of a template rendering should have a single root node:
<!–– ok: result has one single root node ––> <!–– ok: result has one single root node ––>
<t> <t>
<div t-if="someCondition">foo</div> <div t-if="someCondition">foo</div>
<span t-else="">bar</span> <span t-else="1">bar</span>
</t> </t>
``` ```
@@ -500,15 +500,6 @@ will result in :
</div> </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 ### Translations
By default, QWeb specify that templates should be translated. If this behaviour By default, QWeb specify that templates should be translated. If this behaviour
+2 -2
View File
@@ -25,8 +25,8 @@ component should own which part of the state.
Owl's solution to this issue is a centralized store. It is a class that owns 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 some (or all) state, and lets the developer update it in a structured way, with
`actions`. Owl components can then connect to the store to read their relevant `actions`. Owl components can then connect to the store, and will be updated if
state, and they will be rerendered if the state is updated. necessary.
Note: Owl store is inspired by React Redux and VueX. Note: Owl store is inspired by React Redux and VueX.
+4 -3
View File
@@ -91,15 +91,16 @@ 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: 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(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)}}} let debugSetup = {
debugOwl(owl, {
// componentBlackList: /App/, // regexp // componentBlackList: /App/, // regexp
// componentWhiteList: /SomeComponent/, // regexp // componentWhiteList: /SomeComponent/, // regexp
// methodBlackList: ["mounted"], // list of method names // methodBlackList: ["mounted"], // list of method names
// methodWhiteList: ["willStart"], // list of method names // methodWhiteList: ["willStart"], // list of method names
logScheduler: false, // display/mute scheduler logs logScheduler: false, // display/mute scheduler logs
logStore: true, // display/mute store logs logStore: true, // display/mute store logs
}); };
{let o,t="[OWL_DEBUG]";function toStr(o){let t=JSON.stringify(o||{});return t.length>200&&(t=t.slice(0,200)+"..."),t}function debugComponent(o,e,n){let l=`${e}<id=${n}>`,r=o=>(!debugSetup.methodBlackList||!debugSetup.methodBlackList.includes(o))&&!(debugSetup.methodWhiteList&&!debugSetup.methodWhiteList.includes(o));r("constructor")&&console.log(`${t} ${l} constructor, props=${toStr(o.props)}`),r("willStart")&&owl.hooks.onWillStart(()=>{console.log(`${t} ${l} willStart`)}),r("mounted")&&owl.hooks.onMounted(()=>{console.log(`${t} ${l} mounted`)}),r("willUpdateProps")&&owl.hooks.onWillUpdateProps(o=>{console.log(`${t} ${l} willUpdateProps, nextprops=${toStr(o)}`)}),r("willPatch")&&owl.hooks.onWillPatch(()=>{console.log(`${t} ${l} willPatch`)}),r("patched")&&owl.hooks.onPatched(()=>{console.log(`${t} ${l} patched`)}),r("willUnmount")&&owl.hooks.onWillUnmount(()=>{console.log(`${t} ${l} willUnmount`)});const s=o.__render.bind(o);o.__render=function(...o){console.log(`${t} ${l} rendering template`),s(...o)};const u=o.render.bind(o);o.render=function(...o){return console.log(`${t} ${l} render`),u(...o)};const c=o.mount.bind(o);o.mount=function(...o){return console.log(`${t} ${l} mount`),c(...o)}}if(Object.defineProperty(owl.Component,"current",{get:()=>o,set(t){o=t;const e=t.constructor.name;if(debugSetup.componentBlackList&&debugSetup.componentBlackList.test(e))return;if(debugSetup.componentWhiteList&&!debugSetup.componentWhiteList.test(e))return;let n;Object.defineProperty(o,"__owl__",{get:()=>n,set(o){debugComponent(t,e,(n=o).id)}})}}),debugSetup.logScheduler){let o;Object.defineProperty(owl.Component.scheduler,"isRunning",{get:()=>o,set(e){e?console.log(`${t} scheduler: start running tasks queue`):console.log(`${t} scheduler: stop running tasks queue`),o=e}})}if(debugSetup.logStore){let o=owl.Store.prototype.dispatch;owl.Store.prototype.dispatch=function(e,...n){return console.log(`${t} store: action '${e}' dispatched. Payload: '${toStr(n)}'`),o.call(this,e,...n)}}}
``` ```
Note that it is certainly useful to run this code at some point in an application, Note that it is certainly useful to run this code at some point in an application,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "owl-framework", "name": "owl-framework",
"version": "1.0.0-beta4", "version": "1.0.0-alpha5",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "src/index.ts", "main": "src/index.ts",
"engines": { "engines": {
+12 -5
View File
@@ -1,17 +1,25 @@
# 🦉 OWL Roadmap 🦉 # 🦉 OWL Roadmap 🦉
- Current version: 1.0.0-beta4 - Current version: 1.0.0-alpha5
- Status: mostly stable - Status: mostly stable
This roadmap is only an attempt at predicting Owl's future. Everything may This roadmap is only an attempt at predicting Owl's future. Everything may
change! change!
### November 2019
Owl will be used in various Odoo projects. We plan to:
- fix any issues encountered
- maybe cleanup slightly the router API
- improve the documentation
- improve error handling, add more helpful error messages
### December 2019 ### December 2019
If all goes well, Owl is upgraded to beta status. From now on, no API change, If all goes well, Owl will be upgraded to beta status. From then, no API change,
even small, is expected (but it still could happen). even small, is expected.
### End of 2019 ### End of 2019
@@ -33,8 +41,7 @@ Release v1.0
Maybe: Maybe:
- reimplement vdom to use *block* system, like Vue 3, which should make Owl - reimplement vdom to use *block* system, like Vue 3,
much faster
- refactor `QWeb` to use an intermediate representation (some kind of AST) to - refactor `QWeb` to use an intermediate representation (some kind of AST) to
allow additional optimisations. allow additional optimisations.
+25 -60
View File
@@ -36,10 +36,6 @@ export interface Env {
[key: string]: any; [key: string]: any;
} }
interface MountOptions {
position?: "first-child" | "last-child" | "self";
}
/** /**
* This is mostly an internal detail of implementation. The Meta interface is * 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 * useful to typecheck and describe the internal keys used by Owl to manage the
@@ -70,9 +66,10 @@ interface Internal<T extends Env, Props> {
parentLastFiberId: number; parentLastFiberId: number;
// when a rendering is initiated by a parent, it may set variables in 'scope' // when a rendering is initiated by a parent, it may set variables in 'scope'
// (typically when the component is rendered in a slot). We need to // and 'vars' (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. // store that information in case the component would be re-rendered later on.
scope: any; scope: any;
vars: any;
boundHandlers: { [key: number]: any }; boundHandlers: { [key: number]: any };
observer: Observer | null; observer: Observer | null;
@@ -87,8 +84,6 @@ interface Internal<T extends Env, Props> {
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null; refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
} }
export const portalSymbol = Symbol("portal"); // FIXME
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Component // Component
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -105,7 +100,6 @@ export class Component<T extends Env, Props extends {}> {
static env: any = {}; static env: any = {};
// expose scheduler s.t. it can be mocked for testing purposes // expose scheduler s.t. it can be mocked for testing purposes
static scheduler: Scheduler = scheduler; static scheduler: Scheduler = scheduler;
__target: HTMLElement | undefined;
/** /**
* The `el` is the root element of the component. Note that it could be null: * The `el` is the root element of the component. Note that it could be null:
@@ -197,7 +191,8 @@ export class Component<T extends Env, Props extends {}> {
renderFn: qweb.render.bind(qweb, template), renderFn: qweb.render.bind(qweb, template),
classObj: null, classObj: null,
refs: null, refs: null,
scope: null scope: null,
vars: null
}; };
} }
@@ -294,8 +289,7 @@ export class Component<T extends Env, Props extends {}> {
* *
* Note that a component can be mounted an unmounted several times * Note that a component can be mounted an unmounted several times
*/ */
async mount(target: HTMLElement | DocumentFragment, options: MountOptions = {}): Promise<void> { async mount(target: HTMLElement | DocumentFragment): Promise<void> {
const position = options.position || "last-child";
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (__owl__.isMounted) { if (__owl__.isMounted) {
return Promise.resolve(); return Promise.resolve();
@@ -305,19 +299,10 @@ 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)`; message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message); throw new Error(message);
} }
let inserter = const fiber = new Fiber(null, this, false, target);
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; fiber.shouldPatch = false;
if (!__owl__.vnode) { if (!__owl__.vnode) {
this.__prepareAndRender(fiber, () => {}); this.__prepareAndRender(fiber);
} else { } else {
this.__render(fiber); this.__render(fiber);
} }
@@ -412,7 +397,14 @@ export class Component<T extends Env, Props extends {}> {
* willUnmount(). * willUnmount().
*/ */
trigger(eventType: string, payload?: any) { trigger(eventType: string, payload?: any) {
this.__trigger(this, eventType, payload); if (this.el) {
const ev = new OwlEvent(this, eventType, {
bubbles: true,
cancelable: true,
detail: payload
});
this.el.dispatchEvent(ev);
}
} }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -486,30 +478,14 @@ export class Component<T extends Env, Props extends {}> {
} }
} }
} }
/**
* Private trigger method, allows to choose the component which triggered
* the event in the first place
*/
__trigger(component: Component<any, any>, eventType: string, payload?: any) {
if (this.el) {
const ev = new OwlEvent(component, eventType, {
bubbles: true,
cancelable: true,
detail: payload
});
const triggerHook = this.env[portalSymbol as any];
if (triggerHook) {
triggerHook(ev);
}
this.el.dispatchEvent(ev);
}
}
/** /**
* The __updateProps method is called by the t-component directive whenever * The __updateProps method is called by the t-component directive whenever
* it updates a component (so, when the parent template is rerendered). * it updates a component (so, when the parent template is rerendered).
*/ */
async __updateProps(nextProps: Props, parentFiber: Fiber, scope: any): Promise<void> { async __updateProps(nextProps: Props, parentFiber: Fiber, scope: any, vars: any): Promise<void> {
this.__owl__.scope = scope; this.__owl__.scope = scope;
this.__owl__.vars = vars;
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps); const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) { if (shouldUpdate) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
@@ -547,27 +523,18 @@ export class Component<T extends Env, Props extends {}> {
*/ */
__patch(vnode: VNode) { __patch(vnode: VNode) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (this.__target) { const target = __owl__.vnode || document.createElement(vnode.sel!);
if (this.__target.tagName.toLowerCase() !== vnode.sel) { __owl__.vnode = patch(target, vnode);
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 * The __prepare method is only called by the t-component directive, when a
* subcomponent is created. It gets its scope, if any, from the * subcomponent is created. It gets its scope and vars, if any, from the
* parent template. * parent template.
*/ */
__prepare(parentFiber: Fiber, scope: any, cb: CallableFunction): Fiber { __prepare(parentFiber: Fiber, scope: any, vars: any) {
this.__owl__.scope = scope; this.__owl__.scope = scope;
this.__owl__.vars = vars;
const fiber = new Fiber(parentFiber, this, parentFiber.force, null); const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
fiber.shouldPatch = false; fiber.shouldPatch = false;
if (!parentFiber.child) { if (!parentFiber.child) {
@@ -576,8 +543,7 @@ export class Component<T extends Env, Props extends {}> {
parentFiber.lastChild!.sibling = fiber; parentFiber.lastChild!.sibling = fiber;
} }
parentFiber.lastChild = fiber; parentFiber.lastChild = fiber;
this.__prepareAndRender(fiber, cb); return this.__prepareAndRender(fiber);
return fiber;
} }
__getTemplate(qweb: QWeb): string { __getTemplate(qweb: QWeb): string {
@@ -599,7 +565,7 @@ export class Component<T extends Env, Props extends {}> {
} }
return p._template; return p._template;
} }
async __prepareAndRender(fiber: Fiber, cb: CallableFunction) { async __prepareAndRender(fiber: Fiber) {
try { try {
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]); await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
} catch (e) { } catch (e) {
@@ -611,7 +577,6 @@ export class Component<T extends Env, Props extends {}> {
} }
if (!fiber.isCompleted) { if (!fiber.isCompleted) {
this.__render(fiber); this.__render(fiber);
cb();
} }
} }
+62 -19
View File
@@ -1,6 +1,6 @@
import { QWeb } from "../qweb/index"; import { QWeb } from "../qweb/index";
import { INTERP_REGEXP } from "../qweb/compilation_context"; import { INTERP_REGEXP } from "../qweb/compilation_context";
import { makeHandlerCode, MODS_CODE } from "../qweb/extensions"; import { MODS_CODE } from "../qweb/extensions";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-component // t-component
@@ -189,15 +189,15 @@ QWeb.addDirective({
extraNames: ["props"], extraNames: ["props"],
priority: 100, priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean { atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine(`// Component '${value}'`); ctx.addLine("//COMPONENT");
ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true; ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true; ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
ctx.rootContext.shouldDefineScope = true;
let hasDynamicProps = node.getAttribute("t-props") ? true : false; let hasDynamicProps = node.getAttribute("t-props") ? true : false;
// t-on- events and t-transition // t-on- events and t-transition
const events: [string, string][] = []; const events: [string, string[], string, string][] = [];
let transition: string = ""; let transition: string = "";
const attributes = (<Element>node).attributes; const attributes = (<Element>node).attributes;
const props: { [key: string]: string } = {}; const props: { [key: string]: string } = {};
@@ -205,7 +205,13 @@ QWeb.addDirective({
const name = attributes[i].name; const name = attributes[i].name;
const value = attributes[i].textContent!; const value = attributes[i].textContent!;
if (name.startsWith("t-on-")) { if (name.startsWith("t-on-")) {
events.push([name, value]); const [eventName, ...mods] = name.slice(5).split(".");
let extraArgs;
let handlerValue = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
events.push([eventName, mods, handlerValue, extraArgs]);
} else if (name === "t-transition") { } else if (name === "t-transition") {
transition = value; transition = value;
} else if (!name.startsWith("t-")) { } else if (!name.startsWith("t-")) {
@@ -220,6 +226,7 @@ QWeb.addDirective({
let propStr = Object.keys(props) let propStr = Object.keys(props)
.map(k => k + ":" + props[k]) .map(k => k + ":" + props[k])
.join(","); .join(",");
let defID = ctx.generateID();
let componentID = ctx.generateID(); let componentID = ctx.generateID();
const templateKey = ctx.generateTemplateKey(); const templateKey = ctx.generateTemplateKey();
@@ -278,15 +285,32 @@ QWeb.addDirective({
} }
} }
let eventsCode = events let eventsCode = events
.map(function([name, value]) { .map(function([eventName, mods, handlerValue, extraArgs]) {
const { event, handler } = makeHandlerCode( let params = "owner";
ctx, if (extraArgs) {
name, if (ctx.loopNumber) {
value, let argId = ctx.generateID();
false, // we need to evaluate the arguments now, because the handler will
T_COMPONENT_MODS_CODE // be set asynchronously later when the widget is ready, and the
); // context might be different.
return `vn.elm.addEventListener('${event}', ${handler});`; ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`);
params = `owner, arg${argId}`;
} else {
params = `owner, ${ctx.formatExpression(extraArgs)}`;
}
}
let handler = `function (e) {`;
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 += `}`;
return `vn.elm.addEventListener('${eventName}', ${handler});`;
}) })
.join(""); .join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false); const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
@@ -325,9 +349,25 @@ QWeb.addDirective({
} }
// SLOTS // SLOTS
const varDefs: string[] = [];
const hasSlots = node.childNodes.length; 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 scope = hasSlots ? `Object.assign(Object.create(context), scope)` : "undefined"; 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";
}
ctx.addIf(`w${componentID}`); ctx.addIf(`w${componentID}`);
@@ -337,7 +377,8 @@ QWeb.addDirective({
styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`; styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
} }
ctx.addLine( ctx.addLine(
`w${componentID}.__updateProps(props${componentID}, extra.fiber, ${scope})${styleCode};` `w${componentID}.__updateProps(props${componentID}, extra.fiber${scopeVars &&
", " + scopeVars})${styleCode};`
); );
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`); ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
if (registerCode) { if (registerCode) {
@@ -398,14 +439,16 @@ QWeb.addDirective({
} }
} }
ctx.addLine( ctx.addLine(`let def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars});`);
`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 // hack: specify empty remove hook to prevent the node from being removed from the DOM
const insertHook = refExpr ? `insert(vn) {${refExpr}},` : ""; const insertHook = refExpr ? `insert(vn) {${refExpr}},` : "";
ctx.addLine( ctx.addLine(
`let pvnode = h('dummy', {key: ${templateKey}, hook: {${insertHook}remove() {},destroy(vn) {${finalizeComponentCode}}}});` `let pvnode = h('dummy', {key: ${templateKey}, hook: {${insertHook}remove() {},destroy(vn) {${finalizeComponentCode}}}});`
); );
ctx.addLine(`const fiber = w${componentID}.__owl__.currentFiber;`);
ctx.addLine(
`def${defID}.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
if (registerCode) { if (registerCode) {
ctx.addLine(registerCode); ctx.addLine(registerCode);
} }
+55 -39
View File
@@ -46,9 +46,10 @@ export class Fiber {
// scheduler. // scheduler.
counter: number = 0; counter: number = 0;
inserter: (el: HTMLElement) => void | null; target: HTMLElement | null;
scope: any; scope: any;
vars: any;
component: Component<any, any>; component: Component<any, any>;
vnode: VNode | null = null; vnode: VNode | null = null;
@@ -61,13 +62,14 @@ export class Fiber {
error?: Error; error?: Error;
constructor(parent: Fiber | null, component: Component<any, any>, force, inserter) { constructor(parent: Fiber | null, component: Component<any, any>, force, target) {
this.component = component; this.component = component;
this.force = force; this.force = force;
this.inserter = inserter; this.target = target;
const __owl__ = component.__owl__; const __owl__ = component.__owl__;
this.scope = __owl__.scope; this.scope = __owl__.scope;
this.vars = __owl__.vars;
this.root = parent ? parent.root : this; this.root = parent ? parent.root : this;
this.parent = parent; this.parent = parent;
@@ -178,8 +180,9 @@ export class Fiber {
*/ */
complete() { complete() {
let component = this.component; let component = this.component;
let fiber: Fiber = this;
this.isCompleted = true; this.isCompleted = true;
if (!this.inserter && !component.__owl__.isMounted) { if (!this.target && !component.__owl__.isMounted) {
return; return;
} }
@@ -192,49 +195,58 @@ export class Fiber {
this._walk(doWork); this._walk(doWork);
const patchLen = patchQueue.length; const patchLen = patchQueue.length;
// call willPatch hook on each fiber of patchQueue try {
for (let i = 0; i < patchLen; i++) { // call willPatch hook on each fiber of patchQueue
const fiber = patchQueue[i]; for (let i = 0; i < patchLen; i++) {
if (fiber.shouldPatch) { fiber = patchQueue[i];
if (fiber.shouldPatch) {
component = fiber.component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.willPatch();
}
}
// call __patch on each fiber of (reversed) patchQueue
for (let i = patchLen - 1; i >= 0; i--) {
fiber = patchQueue[i];
component = fiber.component; component = fiber.component;
if (component.__owl__.willPatchCB) { component.__patch(fiber.vnode!);
component.__owl__.willPatchCB(); if (!fiber.shouldPatch && (!fiber.target || i !== 0)) {
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
} }
component.willPatch(); component.__owl__.currentFiber = null;
} }
}
// call __patch on each fiber of (reversed) patchQueue // insert into the DOM (mount case)
for (let i = patchLen - 1; i >= 0; i--) { let inDOM = false;
const fiber = patchQueue[i]; if (this.target) {
component = fiber.component; this.target.appendChild(this.component.el!);
component.__patch(fiber.vnode!); inDOM = document.body.contains(this.target);
if (!fiber.shouldPatch && (!fiber.inserter || i !== 0)) {
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
} }
component.__owl__.currentFiber = null;
}
// insert into the DOM (mount case) // call patched/mounted hook on each fiber of (reversed) patchQueue
let inDOM = false; for (let i = patchLen - 1; i >= 0; i--) {
if (this.inserter) { fiber = patchQueue[i];
this.inserter(this.component.el!); component = fiber.component;
inDOM = document.body.contains(this.component.el); if (fiber.shouldPatch && !this.target) {
this.component.env.qweb.trigger("dom-appended"); component.patched();
} if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
// call patched/mounted hook on each fiber of (reversed) patchQueue }
for (let i = patchLen - 1; i >= 0; i--) { } else if (this.target ? inDOM : true) {
const fiber = patchQueue[i]; component.__callMounted();
component = fiber.component;
if (fiber.shouldPatch && !this.inserter) {
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
} }
} else if (this.inserter ? inDOM : true) {
component.__callMounted();
} }
} catch (e) {
// if there is no current fiber on component, we are in the situation where
// components were patched to the DOM, but a mounted/patched hook threw an
// error. In that case, we cannot manage the error at a lower level than
// the root fiber, since some components may not have been properly mounted
// patched yet.
const errorFiber = component.__owl__.currentFiber ? fiber : this;
errorFiber.handleError(e);
} }
} }
@@ -273,7 +285,11 @@ export class Fiber {
qweb.trigger("error", error); qweb.trigger("error", error);
if (canCatch) { if (canCatch) {
// this.root.isCompleted = false
this.root.isCompleted = false;
// component.__owl__.currentFiber!.root.isCompleted = false;
component.catchError!(error); component.catchError!(error);
} else { } else {
// the 3 next lines aim to mark the root fiber as being in error, and // the 3 next lines aim to mark the root fiber as being in error, and
// to force it to end, without waiting for its children // to force it to end, without waiting for its children
+8 -18
View File
@@ -25,15 +25,6 @@ export class Scheduler {
this.requestAnimationFrame = requestAnimationFrame; this.requestAnimationFrame = requestAnimationFrame;
} }
start() {
this.isRunning = true;
this.scheduleTasks();
}
stop() {
this.isRunning = false;
}
addFiber(fiber): Promise<void> { addFiber(fiber): Promise<void> {
// if the fiber was remapped into a larger rendering fiber, it may not be a // if the fiber was remapped into a larger rendering fiber, it may not be a
// root fiber. But we only want to register root fibers // root fiber. But we only want to register root fibers
@@ -52,7 +43,7 @@ export class Scheduler {
} }
}); });
if (!this.isRunning) { if (!this.isRunning) {
this.start(); this.scheduleTasks();
} }
}); });
} }
@@ -71,10 +62,9 @@ export class Scheduler {
} }
if (task.fiber.counter === 0) { if (task.fiber.counter === 0) {
if (!task.fiber.error) { if (!task.fiber.error) {
try { task.fiber.complete();
task.fiber.complete(); if (!task.fiber.isCompleted) {
} catch (e) { return true;
task.fiber.handleError(e);
} }
} }
task.callback(); task.callback();
@@ -83,16 +73,16 @@ export class Scheduler {
return true; return true;
}); });
this.tasks = tasks.concat(this.tasks); this.tasks = tasks.concat(this.tasks);
if (this.tasks.length === 0) {
this.stop();
}
} }
scheduleTasks() { scheduleTasks() {
this.isRunning = true;
this.requestAnimationFrame(() => { this.requestAnimationFrame(() => {
this.flush(); this.flush();
if (this.isRunning) { if (this.tasks.length > 0) {
this.scheduleTasks(); this.scheduleTasks();
} else {
this.isRunning = false;
} }
}); });
} }
+1
View File
@@ -20,6 +20,7 @@
export class Observer { export class Observer {
rev: number = 1; rev: number = 1;
allowMutations: boolean = true; allowMutations: boolean = true;
dirty: boolean = false;
weakMap: WeakMap<any, any> = new WeakMap(); weakMap: WeakMap<any, any> = new WeakMap();
notifyCB() {} notifyCB() {}
+1 -2
View File
@@ -12,7 +12,6 @@ import * as _store from "./store";
import * as _utils from "./utils"; import * as _utils from "./utils";
import * as _tags from "./tags"; import * as _tags from "./tags";
import { AsyncRoot } from "./misc/async_root"; import { AsyncRoot } from "./misc/async_root";
import { Portal } from "./misc/portal";
import * as _hooks from "./hooks"; import * as _hooks from "./hooks";
import * as _context from "./context"; import * as _context from "./context";
import { Link } from "./router/link"; import { Link } from "./router/link";
@@ -30,7 +29,7 @@ export const router = { Router, RouteComponent, Link };
export const Store = _store.Store; export const Store = _store.Store;
export const utils = _utils; export const utils = _utils;
export const tags = _tags; export const tags = _tags;
export const misc = { AsyncRoot, Portal }; export const misc = { AsyncRoot };
export const hooks = Object.assign({}, _hooks, { export const hooks = Object.assign({}, _hooks, {
useContext: _context.useContext, useContext: _context.useContext,
useDispatch: _store.useDispatch, useDispatch: _store.useDispatch,
-167
View File
@@ -1,167 +0,0 @@
import { Component, portalSymbol } from "../component/component";
import { VNode, patch } from "../vdom/index";
import { xml } from "../tags";
import { OwlEvent } from "../core/owl_event";
import { useSubEnv } from "../hooks";
/**
* Portal
*
* The Portal component allows to render a part of a component outside it's DOM.
* It is for example useful for dialogs: for css reasons, dialogs are in general
* placed in a specific spot of the DOM (e.g. directly in the body). With the
* Portal, a component can conditionally specify in its tempate that it contains
* a dialog, and where this dialog should be inserted in the DOM.
*
* The Portal component ensures that the communication between the content of
* the Portal and its parent properly works: business events reaching the Portal
* are re-triggered on an empty <portal> node located in the parent's DOM.
*/
export class Portal extends Component<any, any> {
static template = xml`<portal><t t-slot="default"/></portal>`;
static props = {
target: {
type: String
}
};
// boolean to indicate whether or not we must listen to 'dom-appended' event
// to hook on the moment when the target is inserted into the DOM (because it
// is not when the portal is rendered)
doTargetLookUp: boolean = true;
// set of encountered events that need to be redirected
_handledEvents: Set<string> = new Set();
// function that will be the event's tunnel (needs to be an arrow function to
// avoid having to rebind `this`)
_handlerTunnel: (f: OwlEvent<any>) => void = (ev: OwlEvent<any>) => {
ev.stopPropagation();
this.__trigger(ev.originalComponent, ev.type, ev.detail);
};
// Storing the parent's env
parentEnv: any = null;
// represents the element that is moved somewhere else
portal: VNode | null = null;
// the target where we will move `portal`
target: HTMLElement | null = null;
constructor(parent, props) {
super(parent, props);
this.parentEnv = parent ? parent.env : {};
// put a callback in the env that is propagated to children s.t. portal can
// register an handler to those events just before children will trigger them
useSubEnv({
[portalSymbol]: ev => {
if (!this._handledEvents.has(ev.type)) {
this.portal!.elm!.addEventListener(ev.type, this._handlerTunnel);
this._handledEvents.add(ev.type);
}
}
});
}
/**
* 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
*/
__checkVNodeStructure(vnode: VNode) {
const children = vnode.children!;
let countRealNodes = 0;
for (let child of children) {
if ((child as VNode).sel) {
countRealNodes++;
}
}
if (countRealNodes !== 1) {
throw new Error(`Portal must have exactly one non-text child (has ${countRealNodes})`);
}
}
/**
* Ensure the target is still there at whichever time we render
*/
__checkTargetPresence() {
if (!this.target || !document.contains(this.target)) {
throw new Error(`Could not find any match for "${this.props.target}"`);
}
}
/**
* Move the portal's element to the target
*/
__deployPortal() {
this.__checkTargetPresence();
this.target!.appendChild(this.portal!.elm!);
}
/**
* Override to remove from the DOM the element we have teleported
*
* @override
*/
__destroy(parent) {
if (this.portal && this.portal.elm) {
const displacedElm = this.portal.elm!;
const parent = displacedElm.parentNode;
if (parent) {
parent.removeChild(displacedElm);
}
}
super.__destroy(parent);
}
/**
* Override to patch the element that has been teleported
*
* @override
*/
__patch(vnode) {
if (this.doTargetLookUp) {
const target = document.querySelector(this.props.target);
if (!target) {
this.env.qweb.on("dom-appended", this, () => {
this.doTargetLookUp = false;
this.env.qweb.off("dom-appended", this);
this.target = document.querySelector(this.props.target);
this.__deployPortal();
});
} else {
this.doTargetLookUp = false;
this.target = target;
}
}
this.__checkVNodeStructure(vnode);
const shouldDeploy =
(!this.portal || this.el!.contains(this.portal.elm!)) && !this.doTargetLookUp;
if (!this.doTargetLookUp && !shouldDeploy) {
// Only on pure patching, provided the
// this.target's parent has not been unmounted
this.__checkTargetPresence();
}
const portalPatch = this.portal ? this.portal : document.createElement(vnode.children[0].sel);
this.portal = patch(portalPatch, vnode.children![0] as VNode);
vnode.children = [];
super.__patch(vnode);
if (shouldDeploy) {
this.__deployPortal();
}
}
/**
* Override to set the env
*/
__trigger(component: Component<any, any>, eventType: string, payload?: any) {
const env = this.env;
this.env = this.parentEnv;
super.__trigger(component, eventType, payload);
this.env = env;
}
}
+113 -146
View File
@@ -1,7 +1,6 @@
import { CompilationContext } from "./compilation_context"; import { CompilationContext } from "./compilation_context";
import { QWeb } from "./qweb"; import { QWeb } from "./qweb";
import { htmlToVDOM } from "../vdom/html_to_vdom"; import { htmlToVDOM } from "../vdom/html_to_vdom";
import { QWebVar } from "./expression_parser";
/** /**
* Owl QWeb Directives * Owl QWeb Directives
@@ -20,40 +19,38 @@ import { QWebVar } from "./expression_parser";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-esc and t-raw // 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; QWeb.utils.htmlToVDOM = htmlToVDOM;
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: CompilationContext) { function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: CompilationContext) {
ctx.rootContext.shouldDefineScope = true;
if (value === "0") { if (value === "0") {
if (ctx.parentNode) { const caller = ctx.getCaller();
// the 'zero' magical symbol is where we can find the result of the rendering if (caller) {
// of the body of the t-call. qweb._compileNode(caller, ctx.getInliningContext());
ctx.rootContext.shouldDefineUtils = true; return;
const zeroArgs = ctx.escaping }
? `{text: utils.vDomToString(scope[utils.zero])}` }
: `...scope[utils.zero]`;
ctx.addLine(`c${ctx.parentNode}.push(${zeroArgs});`); if (value.xml instanceof NodeList && !value.id) {
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
} }
return; return;
} }
let exprID: string; let exprID: string;
if (typeof value === "string") { if (typeof value === "string") {
exprID = `_${ctx.generateID()}`; exprID = `_${ctx.generateID()}`;
ctx.addLine(`let ${exprID} = ${ctx.formatExpression(value)};`); ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
} else { } else {
exprID = `scope.${value.id}`; exprID = value.id;
} }
ctx.addIf(`${exprID} != null`); ctx.addIf(`${exprID} || ${exprID} === 0`);
if (ctx.escaping) { 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) { if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`); ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
} else if (ctx.parentNode) { } else if (ctx.parentNode) {
@@ -62,30 +59,26 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
let nodeID = ctx.generateID(); let nodeID = ctx.generateID();
ctx.rootContext.rootNode = nodeID; ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID; ctx.rootContext.parentTextNode = nodeID;
ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`); ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
if (ctx.rootContext.shouldDefineResult) { if (ctx.rootContext.shouldDefineResult) {
ctx.addLine(`result = vn${nodeID}`); ctx.addLine(`result = vn${nodeID}`);
} }
} }
if (value.hasBody) {
ctx.stopProtectScope(protectID);
}
} else { } else {
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
if (value.hasBody) { ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
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) { if (node.childNodes.length) {
ctx.addElse(); ctx.addElse();
qweb._compileChildren(node, ctx); 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(); ctx.closeIf();
} }
@@ -116,46 +109,24 @@ QWeb.addDirective({
name: "set", name: "set",
extraNames: ["value"], extraNames: ["value"],
priority: 60, priority: 60,
atNodeEncounter({ node, qweb, ctx }): boolean { atNodeEncounter({ node, ctx }): boolean {
ctx.rootContext.shouldDefineScope = true;
const variable = node.getAttribute("t-set")!; const variable = node.getAttribute("t-set")!;
let value = node.getAttribute("t-value")!; let value = node.getAttribute("t-value")!;
ctx.variables[variable] = ctx.variables[variable] || ({} as QWebVar); ctx.variables[variable] = ctx.variables[variable] || {};
let qwebvar = ctx.variables[variable]; let qwebvar = ctx.variables[variable];
const hasBody = node.hasChildNodes();
qwebvar.id = variable;
qwebvar.expr = `scope.${variable}`;
if (value) { if (value) {
const formattedValue = ctx.formatExpression(value); const formattedValue = ctx.formatExpression(value);
ctx.addLine(`${qwebvar.expr} = ${formattedValue};`); if (ctx.variables.hasOwnProperty(variable) && qwebvar.id) {
qwebvar.value = formattedValue; ctx.addLine(`${qwebvar.id} = ${formattedValue}`);
} } else {
const varName = `_${ctx.generateID()}`;
if (hasBody) { ctx.addLine(`var ${varName} = ${formattedValue};`);
ctx.rootContext.shouldDefineUtils = true; qwebvar.id = varName;
if (value) { qwebvar.expr = formattedValue;
ctx.addIf(`!(${qwebvar.expr})`);
}
const tempParentNodeID = ctx.generateID();
const _parentNode = ctx.parentNode;
ctx.parentNode = tempParentNodeID;
ctx.addLine(`let 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; return true;
} }
@@ -169,7 +140,7 @@ QWeb.addDirective({
priority: 20, priority: 20,
atNodeEncounter({ node, ctx }): boolean { atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-if")!); let cond = ctx.getValue(node.getAttribute("t-if")!);
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id!}`); ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : cond.id!);
return false; return false;
}, },
finalize({ ctx }) { finalize({ ctx }) {
@@ -182,9 +153,7 @@ QWeb.addDirective({
priority: 30, priority: 30,
atNodeEncounter({ node, ctx }): boolean { atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-elif")!); let cond = ctx.getValue(node.getAttribute("t-elif")!);
ctx.addLine( ctx.addLine(`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : cond.id}) {`);
`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id}`}) {`
);
ctx.indent(); ctx.indent();
return false; return false;
}, },
@@ -213,10 +182,6 @@ QWeb.addDirective({
name: "call", name: "call",
priority: 50, priority: 50,
atNodeEncounter({ node, qweb, ctx }): boolean { atNodeEncounter({ node, qweb, ctx }): boolean {
// Step 1: sanity checks
// ------------------------------------------------
ctx.rootContext.shouldDefineScope = true;
ctx.rootContext.shouldDefineUtils = true;
if (node.nodeName !== "t") { if (node.nodeName !== "t") {
throw new Error("Invalid tag for t-call directive (should be 't')"); throw new Error("Invalid tag for t-call directive (should be 't')");
} }
@@ -225,65 +190,77 @@ QWeb.addDirective({
if (!nodeTemplate) { if (!nodeTemplate) {
throw new Error(`Cannot find template "${subTemplate}" (t-call)`); throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
} }
const nodeCopy = node.cloneNode(true) as Element;
nodeCopy.removeAttribute("t-call");
// Step 2: compile target template in sub templates // extract variables from nodecopy
// ------------------------------------------------ const tempCtx = new CompilationContext();
if (!qweb.subTemplates[subTemplate]) { tempCtx.allowMultipleRoots = true;
qweb.subTemplates[subTemplate] = true; qweb._compileNode(nodeCopy, tempCtx);
const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx, true); const vars = Object.assign({}, ctx.variables, tempCtx.variables);
qweb.subTemplates[subTemplate] = subTemplateFn;
}
// Step 3: compile t-call body if necessary const templateMap = Object.create(ctx.templates);
// ------------------------------------------------ // open new scope, if necessary
let hasBody = node.hasChildNodes(); const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
let protectID;
if (hasBody) { // compile sub template
// we add a sub scope to protect the ambient scope let subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
ctx.addLine(`{`); subCtx = subCtx.subContext("templates", templateMap);
ctx.indent();
protectID = ctx.startProtectScope(); if (templateMap[subTemplate]) {
const nodeCopy = node.cloneNode(true) as Element; // OUCH, IT IS A RECURSIVE TEMPLATE SITUATION...
for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) { // This is a tricky situation... We obviously cannot inline the compiled
nodeCopy.removeAttribute(attr); // 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;
} }
const parentNode = ctx.parentNode; let varCode = `{}`;
ctx.parentNode = "__0"; if (Object.keys(vars).length) {
// this local scope is intended to trap c__0 let id = 0;
ctx.addLine(`{`); 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;
}
templateMap[subTemplate] = true;
if (hasNewVariables) {
ctx.addLine("{");
ctx.indent(); ctx.indent();
ctx.addLine("let c__0 = [];"); // add new variables, if any
qweb._compileNode(nodeCopy, ctx); for (let key in tempCtx.variables) {
ctx.rootContext.shouldDefineUtils = true; const v = tempCtx.variables[key];
ctx.addLine("scope[utils.zero] = c__0;"); if (v.expr) {
ctx.parentNode = parentNode; ctx.addLine(`let ${v.id} = ${v.expr};`);
ctx.dedent(); }
ctx.addLine(`}`); // todo: handle XML variables...
}
} }
qweb._compileNode(nodeTemplate.elem, subCtx);
// Step 4: add the appropriate function call to current component // close new scope
// ------------------------------------------------ if (hasNewVariables) {
const callingScope = hasBody ? "scope" : "Object.assign(Object.create(context), scope)";
const parentComponent = `utils.getComponent(context)`;
const keyCode = ctx.loopNumber || ctx.hasKey0 ? `, key: ${ctx.generateTemplateKey()}` : "";
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}${keyCode}})`;
if (ctx.parentNode) {
ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, ${extra});`);
} 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}, ${extra});`);
ctx.addLine(`result = result[0]`);
}
// Step 5: restore previous scope
// ------------------------------------------------
if (hasBody) {
ctx.stopProtectScope(protectID);
ctx.dedent(); ctx.dedent();
ctx.addLine(`}`); ctx.addLine("}");
} }
return true; return true;
@@ -298,31 +275,29 @@ QWeb.addDirective({
extraNames: ["as"], extraNames: ["as"],
priority: 10, priority: 10,
atNodeEncounter({ node, qweb, ctx }): boolean { atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldDefineScope = true; ctx.rootContext.shouldProtectContext = true;
ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1); ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1);
const elems = node.getAttribute("t-foreach")!; const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!; const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID(); let arrayID = ctx.generateID();
ctx.addLine(`let _${arrayID} = ${ctx.formatExpression(elems)};`); ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`); ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
let keysID = ctx.generateID(); let keysID = ctx.generateID();
let valuesID = ctx.generateID(); let valuesID = ctx.generateID();
ctx.addLine(`let _${keysID} = _${valuesID} = _${arrayID};`); ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
ctx.addIf(`!(_${arrayID} instanceof Array)`); ctx.addIf(`!(_${arrayID} instanceof Array)`);
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`); ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`); ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf(); ctx.closeIf();
ctx.addLine(`let _length${keysID} = _${keysID}.length;`); ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
let varsID = ctx.startProtectScope();
const loopVar = `i${ctx.loopNumber}`; const loopVar = `i${ctx.loopNumber}`;
ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`); ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
ctx.indent(); ctx.indent();
ctx.addToScope(name + "_first", `${loopVar} === 0`);
ctx.addLine(`scope.${name}_first = ${loopVar} === 0`); ctx.addToScope(name + "_last", `${loopVar} === _length${keysID} - 1`);
ctx.addLine(`scope.${name}_last = ${loopVar} === _length${keysID} - 1`); ctx.addToScope(name + "_index", loopVar);
ctx.addLine(`scope.${name}_index = ${loopVar}`); ctx.addToScope(name, `_${keysID}[${loopVar}]`);
ctx.addLine(`scope.${name} = _${keysID}[${loopVar}]`); ctx.addToScope(name + "_value", `_${valuesID}[${loopVar}]`);
ctx.addLine(`scope.${name}_value = _${valuesID}[${loopVar}]`);
const nodeCopy = <Element>node.cloneNode(true); const nodeCopy = <Element>node.cloneNode(true);
let shouldWarn = let shouldWarn =
!nodeCopy.hasAttribute("t-key") && !nodeCopy.hasAttribute("t-key") &&
@@ -334,19 +309,11 @@ QWeb.addDirective({
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')` `Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
); );
} }
if (nodeCopy.hasAttribute("t-key")) {
const expr = ctx.formatExpression(nodeCopy.getAttribute("t-key")!);
ctx.addLine(`let key${ctx.loopNumber} = ${expr};`);
nodeCopy.removeAttribute("t-key");
} else {
ctx.addLine(`let key${ctx.loopNumber} = i${ctx.loopNumber};`);
}
nodeCopy.removeAttribute("t-foreach"); nodeCopy.removeAttribute("t-foreach");
qweb._compileNode(nodeCopy, ctx); qweb._compileNode(nodeCopy, ctx);
ctx.dedent(); ctx.dedent();
ctx.addLine("}"); ctx.addLine("}");
ctx.stopProtectScope(varsID);
return true; return true;
} }
}); });
+72 -30
View File
@@ -10,29 +10,36 @@ export class CompilationContext {
code: string[] = []; code: string[] = [];
variables: { [key: string]: QWebVar } = {}; variables: { [key: string]: QWebVar } = {};
escaping: boolean = false; escaping: boolean = false;
parentNode: number | null | string = null; parentNode: number | null = null;
parentTextNode: number | null = null; parentTextNode: number | null = null;
rootNode: number | null = null; rootNode: number | null = null;
indentLevel: number = 0; indentLevel: number = 0;
rootContext: CompilationContext; rootContext: CompilationContext;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
shouldDefineParent: boolean = false; shouldDefineParent: boolean = false;
shouldDefineScope: boolean = false;
shouldDefineQWeb: boolean = false; shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false; shouldDefineUtils: boolean = false;
shouldDefineRefs: boolean = false; shouldDefineRefs: boolean = false;
shouldDefineResult: boolean = true; shouldDefineResult: boolean = true;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
loopNumber: number = 0; loopNumber: number = 0;
inPreTag: boolean = false; inPreTag: boolean = false;
templateName: string; templateName: string;
allowMultipleRoots: boolean = false; allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false; hasParentWidget: boolean = false;
hasKey0: boolean = false; scopeVars: any[] = [];
keyStack: boolean[] = []; currentKey: string = "";
templates: { [key: string]: boolean } = {};
callingLevel: number = 0;
inliningLevel: number = 0;
constructor(name?: string) { constructor(name?: string) {
this.rootContext = this; this.rootContext = this;
this.templateName = name || "noname"; this.templateName = name || "noname";
this.addLine("let h = this.h;"); this.templates[this.templateName] = true;
this.addLine("var h = this.h;");
} }
generateID(): number { generateID(): number {
@@ -47,31 +54,47 @@ export class CompilationContext {
* Such a key is necessary when we need to associate an id to some element * Such a key is necessary when we need to associate an id to some element
* generated by a template (for example, a component) * generated by a template (for example, a component)
*/ */
generateTemplateKey(prefix: string = ""): string { generateTemplateKey(): string {
const id = this.generateID(); const id = this.generateID();
if (this.loopNumber === 0 && !this.hasKey0) { let locationExpr = `\`__${this.generateID()}__`;
return `'${prefix}__${id}__'`; for (let i = 0; i < this.loopNumber - 1; i++) {
locationExpr += `\${i${i + 1}}__`;
} }
let key = `\`${prefix}__${id}__`; if (this.currentKey) {
let start = this.hasKey0 ? 0 : 1; const k = this.currentKey;
for (let i = start; i < this.loopNumber + 1; i++) { this.addLine(`let k${id} = ${locationExpr}\` + ${k};`);
key += `\${key${i}}__`; } else {
locationExpr += this.loopNumber ? `\${i${this.loopNumber}}__\`` : "`";
this.addLine(`let k${id} = ${locationExpr};`);
} }
this.addLine(`let k${id} = ${key}\`;`);
return `k${id}`; return `k${id}`;
} }
generateCode(): string[] { 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) { if (this.shouldDefineResult) {
this.code.unshift(" let result;"); this.code.unshift(" let result;");
} }
if (this.shouldDefineScope) {
this.code.unshift(" let scope = Object.create(context);");
}
if (this.shouldDefineRefs) { if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};"); 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.shouldDefineParent) {
if (this.hasParentWidget) { if (this.hasParentWidget) {
this.code.unshift(" let parent = extra.parent;"); this.code.unshift(" let parent = extra.parent;");
@@ -108,15 +131,19 @@ export class CompilationContext {
subContext(key: keyof CompilationContext, value: any): CompilationContext { subContext(key: keyof CompilationContext, value: any): CompilationContext {
const newContext = Object.create(this); const newContext = Object.create(this);
newContext[key] = value; newContext[key] = value;
if (key === "caller") {
newContext.callingLevel++;
newContext.inliningLevel++;
}
return newContext; return newContext;
} }
indent() { indent() {
this.rootContext.indentLevel++; this.indentLevel++;
} }
dedent() { dedent() {
this.rootContext.indentLevel--; this.indentLevel--;
} }
addLine(line: string): number { addLine(line: string): number {
@@ -125,6 +152,11 @@ export class CompilationContext {
return this.code.length - 1; 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) { addIf(condition: string) {
this.addLine(`if (${condition}) {`); this.addLine(`if (${condition}) {`);
this.indent(); this.indent();
@@ -140,6 +172,27 @@ export class CompilationContext {
this.dedent(); this.dedent();
this.addLine("}"); 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 { getValue(val: any): QWebVar | string {
return val in this.variables ? this.getValue(this.variables[val]) : val; return val in this.variables ? this.getValue(this.variables[val]) : val;
@@ -152,7 +205,6 @@ export class CompilationContext {
* - replace already defined variables by their internal name * - replace already defined variables by their internal name
*/ */
formatExpression(expr: string): string { formatExpression(expr: string): string {
this.rootContext.shouldDefineScope = true;
return compileExpr(expr, this.variables); return compileExpr(expr, this.variables);
} }
@@ -173,14 +225,4 @@ export class CompilationContext {
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}"); let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`"; return "`" + r + "`";
} }
startProtectScope(): number {
const protectID = this.generateID();
this.rootContext.shouldDefineScope = true;
this.addLine(`let _origScope${protectID} = scope;`);
this.addLine(`scope = Object.assign(Object.create(context), scope);`);
return protectID;
}
stopProtectScope(protectID: number) {
this.addLine(`scope = _origScope${protectID};`);
}
} }
+17 -37
View File
@@ -39,10 +39,9 @@ const WORD_REPLACEMENT = {
}; };
export interface QWebVar { export interface QWebVar {
id: string; // foo id?: string;
expr: string; // scope.foo (local variables => only foo) expr?: string;
value?: string; // 1 + 3 xml?: NodeList;
hasBody?: boolean;
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -64,7 +63,6 @@ type TKind =
interface Token { interface Token {
type: TKind; type: TKind;
value: string; value: string;
originalValue?: string;
size?: number; size?: number;
} }
@@ -81,7 +79,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
// note that the space after typeof is relevant. It makes sure that the formatted // note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof // expression has a space after typeof
const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;".split(","); const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ".split(",");
type Tokenizer = (expr: string) => Token | false; type Tokenizer = (expr: string) => Token | false;
@@ -229,53 +227,35 @@ export function tokenize(expr: string): Token[] {
* - unless the previous token is a dot (in that case, this is a property: `a.b`) * - 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 * - 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}`) * 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, scope: { [key: string]: QWebVar }): string { export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): string {
scope = Object.create(scope);
const tokens = tokenize(expr); const tokens = tokenize(expr);
let result = "";
for (let i = 0; i < tokens.length; i++) { for (let i = 0; i < tokens.length; i++) {
let token = tokens[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)) { 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) {
if (prevToken.type === "OPERATOR" && prevToken.value === ".") { if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false; isVar = false;
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") { } else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
let nextToken = tokens[i + 1];
if (nextToken && nextToken.type === "COLON") { if (nextToken && nextToken.type === "COLON") {
isVar = false; isVar = false;
} }
} }
} }
} if (isVar) {
if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") { if (token.value in vars && "id" in vars[token.value]) {
if (token.type === "RIGHT_PAREN") { token.value = vars[token.value].id!;
let j = i - 1; } else {
while (j > 0 && tokens[j].type !== "LEFT_PAREN") { token.value = `context['${token.value}']`;
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 tokens.map(t => t.value).join(""); return result;
} }
+46 -79
View File
@@ -26,66 +26,49 @@ export const MODS_CODE = {
stop: "e.stopPropagation();" stop: "e.stopPropagation();"
}; };
interface HandlerInfo {
event: string;
handler: string;
}
const FNAMEREGEXP = /^[$A-Z_][0-9A-Z_$]*$/i;
export function makeHandlerCode(
ctx,
fullName,
value,
putInCache: boolean,
modcodes = MODS_CODE
): HandlerInfo {
const [event, ...mods] = fullName.slice(5).split(".");
if (!event) {
throw new Error("Missing event name with t-on directive");
}
let code: string;
// check if it is a method with no args, a method with args or an expression
let args: string = "";
const name: string = value.replace(/\(.*\)/, function(_args) {
args = _args.slice(1, -1);
return "";
});
const isMethodCall = name.match(FNAMEREGEXP);
// then generate code
if (isMethodCall) {
ctx.rootContext.shouldDefineUtils = true;
const comp = `utils.getComponent(context)`;
if (args) {
let argId = ctx.generateID();
ctx.addLine(`let args${argId} = [${ctx.formatExpression(args)}];`);
code = `${comp}['${name}'](...args${argId}, e);`;
putInCache = false;
} else {
code = `${comp}['${name}'](e);`;
}
} else {
// if we get here, then it is an expression
putInCache = false;
code = ctx.formatExpression(value);
}
const modCode = mods.map(mod => modcodes[mod]).join("");
let handler = `function (e) {if (!context.__owl__.isMounted){return}${modCode}${code}}`;
if (putInCache) {
const key = ctx.generateTemplateKey(event);
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || ${handler};`);
handler = `extra.handlers[${key}]`;
}
return { event, handler };
}
QWeb.addDirective({ QWeb.addDirective({
name: "on", name: "on",
priority: 90, priority: 90,
atNodeCreation({ ctx, fullName, value, nodeID }) { atNodeCreation({ ctx, fullName, value, nodeID }) {
const { event, handler } = makeHandlerCode(ctx, fullName, value, true); ctx.rootContext.shouldDefineOwner = true;
ctx.addLine(`p${nodeID}.on['${event}'] = ${handler};`); const [eventName, ...mods] = fullName.slice(5).split(".");
if (!eventName) {
throw new Error("Missing event name with t-on directive");
}
let extraArgs;
let handlerName = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
let handler = `function (e) {`;
handler += mods
.map(function(mod) {
return MODS_CODE[mod];
})
.join("");
if (handlerName) {
if (!extraArgs) {
handler += `const fn = context['${handlerName}'];`;
handler += `if (fn) { fn.call(${params}, e); } else { context.${handlerName}; }`;
handler += `}`;
ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
);
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
} else {
const handlerKey = `handler${ctx.generateID()}`;
ctx.addLine(
`const ${handlerKey} = context['${handlerName}'] && context['${handlerName}'].bind(${params});`
);
handler += `if (${handlerKey}) { ${handlerKey}(e); } else { context.${value}; }`;
handler += `}`;
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
}
} else {
handler += "}";
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
}
} }
}); });
@@ -212,8 +195,9 @@ QWeb.addDirective({
QWeb.addDirective({ QWeb.addDirective({
name: "slot", name: "slot",
priority: 80, priority: 80,
atNodeEncounter({ ctx, value, node, qweb }): boolean { atNodeEncounter({ ctx, value }): boolean {
const slotKey = ctx.generateID(); const slotKey = ctx.generateID();
ctx.rootContext.shouldDefineOwner = true;
ctx.addLine( ctx.addLine(
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];` `const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
); );
@@ -227,17 +211,11 @@ QWeb.addDirective({
ctx.addLine(`result = {}`); ctx.addLine(`result = {}`);
} }
ctx.addLine( ctx.addLine(
`slot${slotKey}.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: ${parentNode}, parent: extra.parent || context}));` `slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: extra.parent || owner}));`
); );
if (!ctx.parentNode) { if (!ctx.parentNode) {
ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`); 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(); ctx.closeIf();
return true; return true;
} }
@@ -303,20 +281,9 @@ QWeb.addDirective({
QWeb.addDirective({ QWeb.addDirective({
name: "key", name: "key",
priority: 45, priority: 45,
atNodeEncounter({ ctx, value, node }) { atNodeEncounter({ ctx, value }) {
if (ctx.loopNumber === 0) { let id = ctx.generateID();
ctx.keyStack.push(ctx.rootContext.hasKey0); ctx.addLine(`const nodeKey${id} = ${ctx.formatExpression(value)};`);
ctx.rootContext.hasKey0 = true; ctx.currentKey = `nodeKey${id}`;
}
ctx.addLine("{");
ctx.indent();
ctx.addLine(`let key${ctx.loopNumber} = ${ctx.formatExpression(value)};`);
},
finalize({ ctx }) {
ctx.dedent();
ctx.addLine("}");
if (ctx.loopNumber === 0) {
ctx.rootContext.hasKey0 = ctx.keyStack.pop() as boolean;
}
} }
}); });
+39 -68
View File
@@ -1,7 +1,7 @@
import { EventBus } from "../core/event_bus"; import { EventBus } from "../core/event_bus";
import { h, patch, VNode } from "../vdom/index"; import { h, patch, VNode } from "../vdom/index";
import { CompilationContext } from "./compilation_context"; import { CompilationContext } from "./compilation_context";
import { shallowEqual, escape } from "../utils"; import { shallowEqual } from "../utils";
import { addNS } from "../vdom/vdom"; import { addNS } from "../vdom/vdom";
/** /**
@@ -87,7 +87,6 @@ interface Utils {
} }
const UTILS: Utils = { const UTILS: Utils = {
zero: Symbol("zero"),
toObj(expr) { toObj(expr) {
if (typeof expr === "string") { if (typeof expr === "string") {
expr = expr.trim(); expr = expr.trim();
@@ -106,26 +105,6 @@ const UTILS: Utils = {
shallowEqual, shallowEqual,
addNameSpace(vnode) { addNameSpace(vnode) {
addNS(vnode.data, vnode.children, vnode.sel); 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("");
},
getComponent(obj) {
while (obj && !obj.hasOwnProperty("__owl__")) {
obj = obj.__proto__;
}
return obj;
} }
}; };
@@ -192,7 +171,7 @@ export class QWeb extends EventBus {
// recursiveTemplates contains sub templates called with t-call, but which // recursiveTemplates contains sub templates called with t-call, but which
// ends up in recursive situations. This is very similar to the slot situation, // ends up in recursive situations. This is very similar to the slot situation,
// as in we need to propagate the scope. // as in we need to propagate the scope.
subTemplates = {}; recursiveFns = {};
isUpdating: boolean = false; isUpdating: boolean = false;
translateFn?: QWebConfig["translateFn"]; translateFn?: QWebConfig["translateFn"];
@@ -354,17 +333,8 @@ export class QWeb extends EventBus {
return vnode.text!; return vnode.text!;
} }
const node = document.createElement(vnode.sel); const node = document.createElement(vnode.sel);
const elem = patch(node, vnode).elm as HTMLElement; const result = patch(node, vnode);
function escapeTextNodes(node) { return (<HTMLElement>result.elm).outerHTML;
if (node.nodeType === 3) {
node.textContent = escape(node.textContent);
}
for (let n of node.childNodes) {
escapeTextNodes(n);
}
}
escapeTextNodes(elem);
return elem.outerHTML;
} }
/** /**
@@ -383,29 +353,31 @@ export class QWeb extends EventBus {
}); });
} }
_compile( _compile(name: string, elem: Element, parentContext?: CompilationContext): CompiledTemplate {
name: string,
elem: Element,
parentContext?: CompilationContext,
defineKey?: boolean
): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug"); const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new CompilationContext(name); const ctx = new CompilationContext(name);
if (elem.tagName !== "t") { if (elem.tagName !== "t") {
ctx.shouldDefineResult = false; ctx.shouldDefineResult = false;
} }
if (parentContext) { if (parentContext) {
ctx.templates = Object.create(parentContext.templates);
ctx.variables = Object.create(parentContext.variables); ctx.variables = Object.create(parentContext.variables);
ctx.parentNode = parentContext.parentNode || ctx.generateID(); ctx.parentNode = parentContext.parentNode || ctx.generateID();
ctx.allowMultipleRoots = true; ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true; ctx.hasParentWidget = true;
ctx.shouldDefineResult = false; ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`); ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
if (defineKey) {
ctx.addLine(`let key0 = extra.key || "";`); for (let v in parentContext.variables) {
ctx.hasKey0 = true; 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); this._compileNode(elem, ctx);
if (!parentContext) { if (!parentContext) {
@@ -420,13 +392,12 @@ export class QWeb extends EventBus {
} }
let code = ctx.generateCode(); let code = ctx.generateCode();
const templateName = ctx.templateName.replace(/`/g, "'").slice(0, 200);
code.unshift(` // Template name: "${templateName}"`);
let template; let template;
try { try {
template = new Function("context, extra", code.join("\n")) as CompiledTemplate; template = new Function("context", "extra", code.join("\n")) as CompiledTemplate;
} catch (e) { } catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`); console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(code.join("\n")); console.warn(code.join("\n"));
console.groupEnd(); console.groupEnd();
@@ -475,7 +446,7 @@ export class QWeb extends EventBus {
// this is an unusual situation: this text node is the result of the // this is an unusual situation: this text node is the result of the
// template rendering. // template rendering.
let nodeID = ctx.generateID(); let nodeID = ctx.generateID();
ctx.addLine(`let vn${nodeID} = {text: \`${text}\`};`); ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
ctx.addLine(`result = vn${nodeID};`); ctx.addLine(`result = vn${nodeID};`);
ctx.rootContext.rootNode = nodeID; ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID; ctx.rootContext.parentTextNode = nodeID;
@@ -483,6 +454,10 @@ export class QWeb extends EventBus {
return; return;
} }
if (ctx !== ctx.rootContext) {
ctx = ctx.subContext("currentKey", ctx.currentKey);
}
const firstLetter = node.tagName[0]; const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) { if (firstLetter === firstLetter.toUpperCase()) {
// this is a component, we modify in place the xml document to change // this is a component, we modify in place the xml document to change
@@ -675,16 +650,15 @@ export class QWeb extends EventBus {
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) { if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
const attID = ctx.generateID(); const attID = ctx.generateID();
if (name === "class") { if (name === "class") {
if ((value = value.trim())) { let classDef = value
let classDef = value .trim()
.split(/\s+/) .split(/\s+/)
.map(a => `'${a}':true`) .map(a => `'${a}':true`)
.join(","); .join(",");
classObj = `_${ctx.generateID()}`; classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`); ctx.addLine(`let ${classObj} = {${classDef}};`);
}
} else { } else {
ctx.addLine(`let _${attID} = '${value}';`); ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) { if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it // attribute contains 'non letters' => we want to quote it
name = '"' + name + '"'; name = '"' + name + '"';
@@ -698,7 +672,7 @@ export class QWeb extends EventBus {
if (name.startsWith("t-att-")) { if (name.startsWith("t-att-")) {
let attName = name.slice(6); let attName = name.slice(6);
const v = ctx.getValue(value); const v = ctx.getValue(value);
let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : `scope.${v.id}`; let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : v.id;
if (attName === "class") { if (attName === "class") {
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
@@ -720,12 +694,12 @@ export class QWeb extends EventBus {
const attValue = (<Element>node).getAttribute(attName); const attValue = (<Element>node).getAttribute(attName);
if (attValue) { if (attValue) {
const attValueID = ctx.generateID(); const attValueID = ctx.generateID();
ctx.addLine(`let _${attValueID} = ${formattedValue};`); ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`; formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":")); const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1); attrs.splice(attrIndex, 1);
} }
ctx.addLine(`let _${attID} = ${formattedValue};`); ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`); attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID); handleBooleanProps(attName, attID);
} }
@@ -741,9 +715,9 @@ export class QWeb extends EventBus {
const attID = ctx.generateID(); const attID = ctx.generateID();
let staticVal = (<Element>node).getAttribute(attName); let staticVal = (<Element>node).getAttribute(attName);
if (staticVal) { if (staticVal) {
ctx.addLine(`let _${attID} = '${staticVal} ' + ${formattedExpr};`); ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
} else { } else {
ctx.addLine(`let _${attID} = ${formattedExpr};`); ctx.addLine(`var _${attID} = ${formattedExpr};`);
} }
attrs.push(`${attName}: _${attID}`); attrs.push(`${attName}: _${attID}`);
} }
@@ -751,13 +725,13 @@ export class QWeb extends EventBus {
// t-att= attributes // t-att= attributes
if (name === "t-att") { if (name === "t-att") {
let id = ctx.generateID(); let id = ctx.generateID();
ctx.addLine(`let _${id} = ${ctx.formatExpression(value!)};`); ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
tattrs.push(id); tattrs.push(id);
} }
} }
let nodeID = ctx.generateID(); let nodeID = ctx.generateID();
let key = ctx.loopNumber || ctx.hasKey0 ? `\`\${key${ctx.loopNumber}}_${nodeID}\`` : nodeID; let nodeKey = ctx.currentKey || nodeID;
const parts = [`key:${key}`]; const parts = [`key:${nodeKey}`];
if (attrs.length + tattrs.length > 0) { if (attrs.length + tattrs.length > 0) {
parts.push(`attrs:{${attrs.join(",")}}`); parts.push(`attrs:{${attrs.join(",")}}`);
} }
@@ -783,12 +757,9 @@ export class QWeb extends EventBus {
ctx.addLine(`}`); ctx.addLine(`}`);
ctx.closeIf(); ctx.closeIf();
} }
ctx.addLine(`let vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`); ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
if (ctx.parentNode) { if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`); ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
} else if (ctx.loopNumber || ctx.hasKey0) {
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`result = vn${nodeID};`);
} }
return nodeID; return nodeID;
-3
View File
@@ -141,9 +141,6 @@ export function useStore(selector, options: SelectorOptions = {}): any {
}, },
set(target, k, v) { set(target, k, v) {
throw new Error("Store state should only be modified through actions"); throw new Error("Store state should only be modified through actions");
},
has(target, k) {
return k in result;
} }
}); });
} }
+6 -3
View File
@@ -58,9 +58,12 @@ export function escape(str: string | number | undefined): string {
if (typeof str === "number") { if (typeof str === "number") {
return String(str); return String(str);
} }
const p = document.createElement("p"); return str
p.textContent = str; .replace(/&/g, "&amp;")
return p.innerHTML; .replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&#x27;")
.replace(/`/g, "&#x60;");
} }
/** /**
+1 -1
View File
@@ -219,7 +219,7 @@ function updateClass(oldVnode: VNode, vnode: VNode): void {
elm = vnode.elm as Element; elm = vnode.elm as Element;
for (name in oldClass) { for (name in oldClass) {
if (name && !klass[name]) { if (!klass[name]) {
elm.classList.remove(name); elm.classList.remove(name);
} }
} }
+95 -91
View File
@@ -1,149 +1,154 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`animations t-transition combined with component 1`] = ` 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 utils = this.constructor.utils;
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let scope = Object.create(context); let owner = context;
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
// Component 'Child' //COMPONENT
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false; let k4 = \`__5__\`;
let props2 = {}; let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) { let props3 = {};
w2.destroy(); if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w2 = false; w3.destroy();
w3 = false;
} }
if (w2) { if (w3) {
w2.__updateProps(props2, extra.fiber, undefined); w3.__updateProps(props3, extra.fiber, undefined, undefined);
let pvnode = w2.__owl__.pvnode; let pvnode = w3.__owl__.pvnode;
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey3 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child']; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w2 = new W2(parent, props2); w3 = new W3(parent, props3);
const __patch2 = w2.__patch; const __patch3 = w3.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}}; w3.__patch = fiber => {__patch3.call(w3, fiber); if(!w3.__owl__.transitionInserted){w3.__owl__.transitionInserted = true;utils.transitionInsert(w3.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap[k4] = w3.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let def2 = w3.__prepare(extra.fiber, undefined, undefined);
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => { let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy(); w3.destroy();
}; };
delete w2.__owl__.transitionInserted; delete w3.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}}); utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w3.__owl__.pvnode = pvnode;
} }
w2.__owl__.parentLastFiberId = extra.fiber.id; w3.__owl__.parentLastFiberId = extra.fiber.id;
return vn1; return vn1;
}" }"
`; `;
exports[`animations t-transition combined with t-component and t-if 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 utils = this.constructor.utils;
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let scope = Object.create(context); let owner = context;
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
if (scope['state'].display) { if (context['state'].display) {
// Component 'Child' //COMPONENT
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false; let k4 = \`__5__\`;
let props2 = {}; let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) { let props3 = {};
w2.destroy(); if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w2 = false; w3.destroy();
w3 = false;
} }
if (w2) { if (w3) {
w2.__updateProps(props2, extra.fiber, undefined); w3.__updateProps(props3, extra.fiber, undefined, undefined);
let pvnode = w2.__owl__.pvnode; let pvnode = w3.__owl__.pvnode;
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey3 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child']; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w2 = new W2(parent, props2); w3 = new W3(parent, props3);
const __patch2 = w2.__patch; const __patch3 = w3.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}}; w3.__patch = fiber => {__patch3.call(w3, fiber); if(!w3.__owl__.transitionInserted){w3.__owl__.transitionInserted = true;utils.transitionInsert(w3.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap[k4] = w3.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let def2 = w3.__prepare(extra.fiber, undefined, undefined);
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => { let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy(); w3.destroy();
}; };
delete w2.__owl__.transitionInserted; delete w3.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}}); utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w3.__owl__.pvnode = pvnode;
} }
w2.__owl__.parentLastFiberId = extra.fiber.id; w3.__owl__.parentLastFiberId = extra.fiber.id;
} }
return vn1; return vn1;
}" }"
`; `;
exports[`animations t-transition combined with t-component, remove and re-add before transitionend 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 utils = this.constructor.utils;
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let scope = Object.create(context); let owner = context;
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
if (scope['state'].flag) { if (context['state'].flag) {
// Component 'Child' //COMPONENT
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false; let k4 = \`__5__\`;
let props2 = {}; let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) { let props3 = {};
w2.destroy(); if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w2 = false; w3.destroy();
w3 = false;
} }
if (w2) { if (w3) {
w2.__updateProps(props2, extra.fiber, undefined); w3.__updateProps(props3, extra.fiber, undefined, undefined);
let pvnode = w2.__owl__.pvnode; let pvnode = w3.__owl__.pvnode;
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey3 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child']; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w2 = new W2(parent, props2); w3 = new W3(parent, props3);
const __patch2 = w2.__patch; const __patch3 = w3.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}}; w3.__patch = fiber => {__patch3.call(w3, fiber); if(!w3.__owl__.transitionInserted){w3.__owl__.transitionInserted = true;utils.transitionInsert(w3.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap[k4] = w3.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let def2 = w3.__prepare(extra.fiber, undefined, undefined);
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => { let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy(); w3.destroy();
}; };
delete w2.__owl__.transitionInserted; delete w3.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}}); utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w3.__owl__.pvnode = pvnode;
} }
w2.__owl__.parentLastFiberId = extra.fiber.id; w3.__owl__.parentLastFiberId = extra.fiber.id;
} }
return vn1; return vn1;
}" }"
`; `;
exports[`animations t-transition with no delay/duration 1`] = ` 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; let utils = this.constructor.utils;
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
p1.hook = { p1.hook = {
insert: vn => { insert: vn => {
utils.transitionInsert(vn, 'jupiler'); utils.transitionInsert(vn, 'jupiler');
@@ -158,13 +163,12 @@ exports[`animations t-transition with no delay/duration 1`] = `
`; `;
exports[`animations t-transition, on a simple node (insert) 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; let utils = this.constructor.utils;
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
p1.hook = { p1.hook = {
insert: vn => { insert: vn => {
utils.transitionInsert(vn, 'chimay'); utils.transitionInsert(vn, 'chimay');
+6 -6
View File
@@ -254,11 +254,11 @@ describe("animations", () => {
widget.state.display = false; widget.state.display = false;
patchNextFrame(cb => { patchNextFrame(cb => {
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="__3__">blue</span></div>' '<div><span class="chimay-leave chimay-leave-active" data-owl-key="__5__">blue</span></div>'
); );
cb(); cb();
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>' '<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__5__">blue</span></div>'
); );
def.resolve(); def.resolve();
}); });
@@ -371,7 +371,7 @@ describe("animations", () => {
await def; // wait for the mocked repaint to be done await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__3__">blue</span></div>'); expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__5__">blue</span></div>');
}); });
test("transitionInsert is called the correct amount of times", async () => { test("transitionInsert is called the correct amount of times", async () => {
@@ -405,14 +405,14 @@ describe("animations", () => {
widget.state.flag = false; widget.state.flag = false;
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>' '<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__5__">blue</span></div>'
); );
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1); expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = true; widget.state.flag = true;
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__3__">blue</span></div>' '<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__5__">blue</span></div>'
); );
expect(QWeb.utils.transitionInsert).toBeCalledTimes(2); expect(QWeb.utils.transitionInsert).toBeCalledTimes(2);
@@ -423,7 +423,7 @@ describe("animations", () => {
expect(QWeb.utils.transitionInsert).toBeCalledTimes(3); expect(QWeb.utils.transitionInsert).toBeCalledTimes(3);
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__3__">blue</span></div>'); expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__5__">blue</span></div>');
QWeb.utils.transitionInsert = oldTransitionInsert; QWeb.utils.transitionInsert = oldTransitionInsert;
}); });
}); });
File diff suppressed because it is too large Load Diff
@@ -1,39 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`props validation props are validated in dev mode (code snapshot) 1`] = ` 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 utils = this.constructor.utils;
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let scope = Object.create(context); let owner = context;
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
// Component 'Child' //COMPONENT
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false; let k4 = \`__5__\`;
let props2 = {message:1}; let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) { let props3 = {message:1};
w2.destroy(); if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w2 = false; w3.destroy();
w3 = false;
} }
if (w2) { if (w3) {
w2.__updateProps(props2, extra.fiber, undefined); w3.__updateProps(props3, extra.fiber, undefined, undefined);
let pvnode = w2.__owl__.pvnode; let pvnode = w3.__owl__.pvnode;
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey3 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child']; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w2 = new W2(parent, props2); w3 = new W3(parent, props3);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap[k4] = w3.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let def2 = w3.__prepare(extra.fiber, undefined, undefined);
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {w3.destroy();}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w3.__owl__.pvnode = pvnode;
} }
w2.__owl__.parentLastFiberId = extra.fiber.id; w3.__owl__.parentLastFiberId = extra.fiber.id;
return vn1; return vn1;
}" }"
`; `;
@@ -1,488 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`t-slot directive can define and call slots 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"Parent\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Dialog'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Dialog\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Dialog'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`t-slot directive can define and call slots 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"Dialog\\"
let h = this.h;
let c6 = [], p6 = {key:6};
let vn6 = h('div', p6, c6);
let c7 = [], p7 = {key:7};
let vn7 = h('div', p7, c7);
c6.push(vn7);
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot8) {
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context}));
}
let c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9);
c6.push(vn9);
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot10) {
slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
}
return vn6;
}"
`;
exports[`t-slot directive can define and call slots 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_header_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`header\`});
}"
`;
exports[`t-slot directive can define and call slots 4`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`footer\`});
}"
`;
exports[`t-slot directive content is the default slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts rocks\`});
}"
`;
exports[`t-slot directive dafault slots can define a default content 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let h = this.h;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot5) {
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context}));
} else {
c4.push({text: \`default content\`});
}
return vn4;
}"
`;
exports[`t-slot directive default slot work with text nodes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let h = this.h;
let c1 = extra.parentNode;
c1.push({text: \`sts rocks\`});
}"
`;
exports[`t-slot directive multiple roots are allowed in a default slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts\`});
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`rocks\`});
}"
`;
exports[`t-slot directive multiple roots are allowed in a named slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_content_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts\`});
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`rocks\`});
}"
`;
exports[`t-slot directive named slots can define a default content 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let h = this.h;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot5) {
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context}));
} else {
c4.push({text: \`default content\`});
}
return vn4;
}"
`;
exports[`t-slot directive refs are properly bound in slots 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils;
context.__owl__.refs = context.__owl__.refs || {};
let h = this.h;
let c1 = extra.parentNode;
let c8 = [], p8 = {key:8,on:{}};
let vn8 = h('button', p8, c8);
c1.push(vn8);
extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p8.on['click'] = extra.handlers['click__9__'];
const ref10 = \`myButton\`;
p8.hook = {
create: (_, n) => {
context.__owl__.refs[ref10] = n.elm;
},
destroy: () => {
delete context.__owl__.refs[ref10];
},
};
c8.push({text: \`do something\`});
}"
`;
exports[`t-slot directive slots are rendered with proper context 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils;
let h = this.h;
let c1 = extra.parentNode;
let c8 = [], p8 = {key:8,on:{}};
let vn8 = h('button', p8, c8);
c1.push(vn8);
extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p8.on['click'] = extra.handlers['click__9__'];
c8.push({text: \`do something\`});
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 2 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"Link\\"
let scope = Object.create(context);
let h = this.h;
let _11 = scope['props'].to;
let c12 = [], p12 = {key:12,attrs:{href: _11}};
let vn12 = h('a', p12, c12);
const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot13) {
slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
}
return vn12;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"App\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2};
let vn2 = h('u', p2, c2);
c1.push(vn2);
let _3 = scope['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
let _4 = _5 = _3;
if (!(_3 instanceof Array)) {
_4 = Object.keys(_3);
_5 = Object.values(_3);
}
let _length4 = _4.length;
let _origScope6 = scope;
scope = Object.assign(Object.create(context), scope);
for (let i1 = 0; i1 < _length4; i1++) {
scope.user_first = i1 === 0
scope.user_last = i1 === _length4 - 1
scope.user_index = i1
scope.user = _4[i1]
scope.user_value = _5[i1]
let key1 = scope['user'].id;
let c7 = [], p7 = {key:\`\${key1}_7\`};
let vn7 = h('li', p7, c7);
c2.push(vn7);
// Component 'Link'
let k9 = \`__9__\${key1}__\`;
let w8 = k9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k9]] : false;
let props8 = {to:'/user/'+scope['user'].id};
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) {
w8.destroy();
w8 = false;
}
if (w8) {
w8.__updateProps(props8, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w8.__owl__.pvnode;
c7.push(pvnode);
} else {
let componentKey8 = \`Link\`;
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| scope['Link'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
w8 = new W8(parent, props8);
parent.__owl__.cmap[k9] = w8.__owl__.id;
w8.__owl__.slotId = 1;
let fiber = w8.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
c7.push(pvnode);
w8.__owl__.pvnode = pvnode;
}
w8.__owl__.parentLastFiberId = extra.fiber.id;
}
scope = _origScope6;
return vn1;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 2 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let scope = Object.create(context);
let h = this.h;
let c7 = extra.parentNode;
c7.push({text: \`User \`});
let _10 = scope['user'].name;
if (_10 != null) {
c7.push({text: _10});
}
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 3 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"Link\\"
let scope = Object.create(context);
let h = this.h;
let _10 = scope['props'].to;
let c11 = [], p11 = {key:11,attrs:{href: _10}};
let vn11 = h('a', p11, c11);
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot12) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c11, parent: extra.parent || context}));
}
return vn11;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"App\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2};
let vn2 = h('u', p2, c2);
c1.push(vn2);
let _3 = scope['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
let _4 = _5 = _3;
if (!(_3 instanceof Array)) {
_4 = Object.keys(_3);
_5 = Object.values(_3);
}
let _length4 = _4.length;
let _origScope6 = scope;
scope = Object.assign(Object.create(context), scope);
for (let i1 = 0; i1 < _length4; i1++) {
scope.user_first = i1 === 0
scope.user_last = i1 === _length4 - 1
scope.user_index = i1
scope.user = _4[i1]
scope.user_value = _5[i1]
let key1 = scope['user'].id;
let c7 = [], p7 = {key:\`\${key1}_7\`};
let vn7 = h('li', p7, c7);
c2.push(vn7);
scope.userdescr = 'User '+scope['user'].name;
// Component 'Link'
let k9 = \`__9__\${key1}__\`;
let w8 = k9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k9]] : false;
let props8 = {to:'/user/'+scope['user'].id};
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) {
w8.destroy();
w8 = false;
}
if (w8) {
w8.__updateProps(props8, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w8.__owl__.pvnode;
c7.push(pvnode);
} else {
let componentKey8 = \`Link\`;
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| scope['Link'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
w8 = new W8(parent, props8);
parent.__owl__.cmap[k9] = w8.__owl__.id;
w8.__owl__.slotId = 1;
let fiber = w8.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
c7.push(pvnode);
w8.__owl__.pvnode = pvnode;
}
w8.__owl__.parentLastFiberId = extra.fiber.id;
}
scope = _origScope6;
return vn1;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 3 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let scope = Object.create(context);
let h = this.h;
let c7 = extra.parentNode;
if (scope.userdescr != null) {
c7.push({text: scope.userdescr});
}
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 4 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"App\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
scope.userdescr = 'User '+scope['state'].user.name;
// Component 'Link'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {to:'/user/'+scope['state'].user.id};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Link\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Link'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let scope = Object.create(context);
let h = this.h;
let c1 = extra.parentNode;
if (scope.userdescr != null) {
c1.push({text: scope.userdescr});
}
}"
`;
exports[`t-slot directive template can just return a slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let result;
let h = this.h;
const slot6 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot6) {
let children7= []
result = {}
slot6.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: children7, parent: extra.parent || context}));
utils.defineProxy(result, children7[0]);
}
return result;
}"
`;
File diff suppressed because it is too large Load Diff
-883
View File
@@ -1,883 +0,0 @@
import { Component, Env } from "../../src/component/component";
import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags";
import { useState, useRef } from "../../src/hooks";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
env.qweb.addTemplate("Component<any,any>", "<div></div>");
env.qweb.addTemplate(
"Counter",
`<div><t t-esc="state.counter"/><button t-on-click="inc">Inc</button></div>`
);
env.qweb.addTemplate("WidgetA", `<div>Hello<t t-component="b"/></div>`);
env.qweb.addTemplate("WidgetB", `<div>world</div>`);
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
function children(w: Component<any, any>): Component<any, any>[] {
const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]);
}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("t-slot directive", () => {
test("can define and call slots", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog>
<t t-set="header"><span>header</span></t>
<t t-set="footer"><span>footer</span></t>
</Dialog>
</div>
<div t-name="Dialog">
<div><t t-slot="header"/></div>
<div><t t-slot="footer"/></div>
</div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div><div><span>header</span></div><div><span>footer</span></div></div></div>"
);
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Dialog.fn.toString()).toMatchSnapshot();
expect(QWeb.slots["1_header"].toString()).toMatchSnapshot();
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>
<div t-name="Parent">
<span class="counter"><t t-esc="state.val"/></span>
<Dialog>
<t t-set="footer"><button t-on-click="doSomething">do something</button></t>
</Dialog>
</div>
<span t-name="Dialog"><t t-slot="footer"/></span>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
state = useState({ val: 0 });
doSomething() {
this.state.val++;
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><span class="counter">0</span><span><button>do something</button></span></div>'
);
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><span class="counter">1</span><span><button>do something</button></span></div>'
);
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("slots are rendered with proper context, part 2", async () => {
env.qweb.addTemplates(`
<templates>
<a t-name="Link" t-att-href="props.to">
<t t-slot="default"/>
</a>
<div t-name="App">
<u><li t-foreach="state.users" t-as="user" t-key="user.id">
<Link to="'/user/' + user.id">User <t t-esc="user.name"/></Link>
</li></u>
</div>
</templates>
`);
class Link extends Component<any, any> {}
class App extends Component<any, any> {
state = useState({
users: [
{ id: 1, name: "Aaron" },
{ id: 2, name: "David" }
]
});
static components = { Link };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User David</a></li></u></div>'
);
expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.users[1].name = "Mathieu";
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User Mathieu</a></li></u></div>'
);
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("slots are rendered with proper context, part 3", async () => {
env.qweb.addTemplates(`
<templates>
<a t-name="Link" t-att-href="props.to">
<t t-slot="default"/>
</a>
<div t-name="App">
<u><li t-foreach="state.users" t-as="user" t-key="user.id" >
<t t-set="userdescr" t-value="'User ' + user.name"/>
<Link to="'/user/' + user.id"><t t-esc="userdescr"/></Link>
</li></u>
</div>
</templates>
`);
class Link extends Component<any, any> {}
class App extends Component<any, any> {
state = useState({
users: [
{ id: 1, name: "Aaron" },
{ id: 2, name: "David" }
]
});
static components = { Link };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User David</a></li></u></div>'
);
expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.users[1].name = "Mathieu";
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User Mathieu</a></li></u></div>'
);
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("slots are rendered with proper context, part 4", async () => {
env.qweb.addTemplates(`
<templates>
<a t-name="Link" t-att-href="props.to">
<t t-slot="default"/>
</a>
<div t-name="App">
<t t-set="userdescr" t-value="'User ' + state.user.name"/>
<Link to="'/user/' + state.user.id"><t t-esc="userdescr"/></Link>
</div>
</templates>
`);
class Link extends Component<any, any> {}
class App extends Component<any, any> {
state = useState({ user: { id: 1, name: "Aaron" } });
static components = { Link };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe('<div><a href="/user/1">User Aaron</a></div>');
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.user.name = "David";
await nextTick();
expect(fixture.innerHTML).toBe('<div><a href="/user/1">User David</a></div>');
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("refs are properly bound in slots", async () => {
class Dialog extends Component<any, any> {
static template = xml`<span><t t-slot="footer"/></span>`;
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<span class="counter"><t t-esc="state.val"/></span>
<Dialog>
<t t-set="footer"><button t-ref="myButton" t-on-click="doSomething">do something</button></t>
</Dialog>
</div>
`;
static components = { Dialog };
state = useState({ val: 0 });
button = useRef("myButton");
doSomething() {
this.state.val++;
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><span class="counter">0</span><span><button>do something</button></span></div>'
);
parent.button.el!.click();
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><span class="counter">1</span><span><button>do something</button></span></div>'
);
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("content is the default slot", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog>
<span>sts rocks</span>
</Dialog>
</div>
<div t-name="Dialog"><t t-slot="default"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>sts rocks</span></div></div>");
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("default slot work with text nodes", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog>sts rocks</Dialog>
</div>
<div t-name="Dialog"><t t-slot="default"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>sts rocks</div></div>");
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("multiple roots are allowed in a named slot", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog>
<t t-set="content">
<span>sts</span>
<span>rocks</span>
</t>
</Dialog>
</div>
<div t-name="Dialog"><t t-slot="content"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>sts</span><span>rocks</span></div></div>");
expect(QWeb.slots["1_content"].toString()).toMatchSnapshot();
});
test("multiple roots are allowed in a default slot", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog>
<span>sts</span>
<span>rocks</span>
</Dialog>
</div>
<div t-name="Dialog"><t t-slot="default"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>sts</span><span>rocks</span></div></div>");
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("missing slots are ignored", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog/>
</div>
<span t-name="Dialog">
<t t-slot="default"/>
<span>some content</span>
<t t-slot="footer"/>
</span>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span><span>some content</span></span></div>");
});
test("t-debug on a t-set (defining a slot)", async () => {
const consoleLog = console.log;
console.log = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog><t t-set="content" t-debug="">abc</t></Dialog>
</div>
<span t-name="Dialog">
<t t-slot="content"/>
</span>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(console.log).toHaveBeenCalledTimes(0);
console.log = consoleLog;
});
test("slot preserves properly parented relationship", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Child>
<GrandChild/>
</Child>
</div>
<div t-name="Child"><t t-slot="default"/></div>
<div t-name="GrandChild">Grand Child</div>
</templates>
`);
class Child extends Component<any, any> {}
class GrandChild extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Child, GrandChild };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><div>Grand Child</div></div></div>");
const parentChildren = children(parent);
expect(parentChildren.length).toBe(1);
expect(parentChildren[0]).toBeInstanceOf(Child);
const childrenChildren = children(parentChildren[0]);
expect(childrenChildren.length).toBe(1);
expect(childrenChildren[0]).toBeInstanceOf(GrandChild);
});
test("nested slots: evaluation context and parented relationship", async () => {
let slot;
class Slot extends Component<any, any> {
static template = xml`<span t-esc="props.val"/>`;
constructor(parent, props) {
super(parent, props);
slot = this;
}
}
class GrandChild extends Component<any, any> {
static template = xml`<div><t t-slot="default"/></div>`;
}
class Child extends Component<any, any> {
static components = { GrandChild };
static template = xml`
<GrandChild>
<t t-slot="default"/>
</GrandChild>`;
}
class Parent extends Component<any, any> {
static components = { Child, Slot };
static template = xml`<Child><Slot val="state.val"/></Child>`;
state = useState({ val: 3 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
expect(slot.__owl__.parent).toBeInstanceOf(GrandChild);
});
test("slot are properly rendered if inner props are changed", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
SC:<t t-esc="props.val"/>
</div>
<div t-name="GenericComponent">
<t t-slot="default" />
</div>
<div t-name="App">
<button t-on-click="inc">Inc[<t t-esc="state.val"/>]</button>
<GenericComponent>
<SomeComponent val="state.val"/>
</GenericComponent>
</div>
</templates>
`);
class SomeComponent extends Component<any, any> {}
class GenericComponent extends Component<any, any> {}
class App extends Component<any, any> {
static components = { GenericComponent, SomeComponent };
state = useState({ val: 4 });
inc() {
this.state.val++;
}
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>Inc[4]</button><div><div> SC:4</div></div></div>");
(<any>fixture.querySelector("button")).click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>Inc[5]</button><div><div> SC:5</div></div></div>");
});
test("slots and wrapper components", async () => {
class Link extends Component<any, any> {
static template = xml`
<a href="abc">
<t t-slot="default"/>
</a>`;
}
class A extends Component<any, any> {
static template = xml`<Link>hey</Link>`;
static components = { Link: Link };
}
const a = new A();
await a.mount(fixture);
expect(fixture.innerHTML).toBe(`<a href="abc">hey</a>`);
});
test("template can just return a slot", async () => {
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.value"/></span>`;
}
class SlotComponent extends Component<any, any> {
static template = xml`<t t-slot="default"/>`;
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<SlotComponent><Child value="state.value"/></SlotComponent>
</div>`;
static components = { SlotComponent, Child };
state = useState({ value: 3 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
expect(QWeb.TEMPLATES[SlotComponent.template].fn.toString()).toMatchSnapshot();
parent.state.value = 5;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>5</span></div>");
});
test("multiple slots containing components", async () => {
class C extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class B extends Component<any, any> {
static template = xml`<div><t t-slot="s1"/><t t-slot="s2"/></div>`;
}
class A extends Component<any, any> {
static template = xml`
<B>
<t t-set="s1"><C val="1"/></t>
<t t-set="s2"><C val="2"/></t>
</B>`;
static components = { B, C };
}
const a = new A();
await a.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><span>1</span><span>2</span></div>`);
});
test("slots in t-foreach and re-rendering", async () => {
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="state.val"/><t t-slot="default"/></span>`;
state = useState({ val: "A" });
mounted() {
this.state.val = "B";
}
}
class Parent extends Component<any, any> {
static components = { Child };
static template = xml`
<div>
<t t-foreach="Array(2)" t-as="n" t-key="n_index">
<Child><t t-esc="n_index"/></Child>
</t>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>A0</span><span>A1</span></div>");
await nextTick(); // wait for the changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("<div><span>B0</span><span>B1</span></div>");
});
test("slots in t-foreach with t-set and re-rendering", async () => {
class Child extends Component<any, any> {
static template = xml`
<span>
<t t-esc="state.val"/>
<t t-slot="default"/>
</span>`;
state = useState({ val: "A" });
mounted() {
this.state.val = "B";
}
}
class ParentWidget extends Component<any, any> {
static components = { Child };
static template = xml`
<div>
<t t-foreach="Array(2)" t-as="n" t-key="n_index">
<t t-set="dummy" t-value="n_index"/>
<Child><t t-esc="dummy"/></Child>
</t>
</div>`;
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>A0</span><span>A1</span></div>");
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 Component<any, any> {
static template = xml`
<span id="c1">
<div>
<t t-slot="default"/>
</div>
</span>`;
constructor(parent, props) {
super(parent, props);
child = this;
}
}
class Child2 extends Component<any, any> {
static template = xml`
<span id="c2">
<t t-slot="default"/>
</span>`;
constructor(parent, props) {
super(parent, props);
child2 = this;
}
}
class Child3 extends Component<any, any> {
static template = xml`
<span>Child 3</span>`;
constructor(parent, props) {
super(parent, props);
child3 = this;
}
}
class Parent extends Component<any, any> {
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 Component<any, any> {
static template = xml`
<span>Child 3</span>`;
constructor(parent, props) {
super(parent, props);
child3 = this;
}
}
class Modal extends Component<any, any> {
static template = xml`
<span id="modal">
<t t-slot="default"/>
</span>`;
constructor(parent, props) {
super(parent, props);
modal = this;
}
}
class Portal extends Component<any, any> {
static template = xml`
<span id="portal">
<t t-slot="default"/>
</span>`;
constructor(parent, props) {
super(parent, props);
portal = this;
}
}
class Dialog extends Component<any, any> {
static components = { Modal, Portal };
static template = xml`
<span id="c2">
<Modal>
<Portal>
<t t-slot="default"/>
</Portal>
</Modal>
</span>`;
}
class Parent extends Component<any, any> {
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 Component<any, any> {
static template = xml`
<span>Child 3</span>`;
constructor(parent, props) {
super(parent, props);
child3 = this;
}
}
class Dialog extends Component<any, any> {
static template = xml`
<span id="c2">
<t t-slot="default"/>
</span>`;
}
class Parent extends Component<any, any> {
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>");
});
});
+6 -10
View File
@@ -1,4 +1,4 @@
import { Env, Component } from "../src/component/component"; import { Env } from "../src/component/component";
import { scheduler } from "../src/component/scheduler"; import { scheduler } from "../src/component/scheduler";
import { EvalContext, QWeb } from "../src/qweb/qweb"; import { EvalContext, QWeb } from "../src/qweb/qweb";
import { CompilationContext } from "../src/qweb/compilation_context"; import { CompilationContext } from "../src/qweb/compilation_context";
@@ -25,7 +25,6 @@ beforeEach(() => {
slots = Object.assign({}, QWeb.slots); slots = Object.assign({}, QWeb.slots);
nextId = QWeb.nextId; nextId = QWeb.nextId;
TEMPLATES = Object.assign({}, QWeb.TEMPLATES); TEMPLATES = Object.assign({}, QWeb.TEMPLATES);
Component.scheduler.tasks = [];
}); });
afterEach(() => { afterEach(() => {
@@ -33,7 +32,6 @@ afterEach(() => {
QWeb.slots = slots; QWeb.slots = slots;
QWeb.nextId = nextId; QWeb.nextId = nextId;
QWeb.TEMPLATES = TEMPLATES; QWeb.TEMPLATES = TEMPLATES;
Component.scheduler.tasks = [];
}); });
// helpers // helpers
@@ -89,11 +87,6 @@ export function renderToDOM(
context: EvalContext = {}, context: EvalContext = {},
extra?: any extra?: any
): HTMLElement | Text { ): 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); const vnode = qweb.render(template, context, extra);
// we snapshot here the compiled code. This is useful to prevent unwanted code // we snapshot here the compiled code. This is useful to prevent unwanted code
@@ -123,8 +116,11 @@ export function renderToString(
context: EvalContext = {}, context: EvalContext = {},
extra?: any extra?: any
): string { ): string {
const result = qweb.renderToString(t, context, extra); const node = renderToDOM(qweb, t, context, extra);
expect(qweb.templates[t].fn.toString()).toMatchSnapshot(); const result = node instanceof Text ? node.textContent! : node.outerHTML;
if (result !== qweb.renderToString(t, context, extra)) {
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
}
return result; return result;
} }
-890
View File
@@ -1,890 +0,0 @@
import { Portal } from "../../src/misc/portal";
import { xml } from "../../src/tags";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
import { Component } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { QWeb } from "../../src/qweb";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - outside: a div with id #outside appended into fixture, meant to be used as
// target by Portal component
// - a test env, necessary to create components, that is set on Component
let fixture: HTMLElement;
let outside: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
outside = document.createElement("div");
outside.setAttribute("id", "outside");
fixture.appendChild(outside);
Component.env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
describe("Portal: Props validation", () => {
test("target is mandatory", async () => {
const dev = QWeb.dev;
QWeb.dev = true;
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal>
<div>2</div>
</Portal>
</div>`;
}
let error;
try {
const parent = new Parent();
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'target' (component 'Portal')`);
QWeb.dev = dev;
});
test("target is not list", async () => {
const dev = QWeb.dev;
QWeb.dev = true;
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="['body']">
<div>2</div>
</Portal>
</div>`;
}
let error;
try {
const parent = new Parent();
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Invalid Prop 'target' in component 'Portal'`);
QWeb.dev = dev;
});
});
describe("Portal: Basic use and DOM placement", () => {
test("basic use of portal", async () => {
const dev = QWeb.dev;
QWeb.dev = true;
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<span>1</span>
<Portal target="'#outside'">
<div>2</div>
</Portal>
</div>`;
}
let error;
let parent;
try {
parent = new Parent();
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<div>2</div>");
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
QWeb.dev = dev;
});
test("conditional use of Portal", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<span>1</span>
<Portal target="'#outside'" t-if="state.hasPortal">
<div>2</div>
</Portal>
</div>`;
state = useState({ hasPortal: false });
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("");
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
parent.state.hasPortal = true;
await nextTick();
expect(outside.innerHTML).toBe("<div>2</div>");
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
parent.state.hasPortal = false;
await nextTick();
expect(outside.innerHTML).toBe("");
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
parent.state.hasPortal = true;
await nextTick();
expect(outside.innerHTML).toBe("<div>2</div>");
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
});
test("conditional use of Portal (with sub Component)", async () => {
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="props.val"/></div>`;
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<span>1</span>
<Portal t-if="state.hasPortal" target="'#outside'">
<Child val="state.val"/>
</Portal>
</div>`;
state = useState({ hasPortal: false, val: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("");
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
parent.state.hasPortal = true;
await nextTick();
expect(outside.innerHTML).toBe("<div>1</div>");
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
parent.state.hasPortal = false;
await nextTick();
expect(outside.innerHTML).toBe("");
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
parent.state.val = 2;
await nextTick();
expect(outside.innerHTML).toBe("");
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
parent.state.hasPortal = true;
await nextTick();
expect(outside.innerHTML).toBe("<div>2</div>");
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
});
test("with target in template (before portal)", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<div id="local-target"></div>
<span>1</span>
<Portal target="'#local-target'">
<p>2</p>
</Portal>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(parent.el!.innerHTML).toBe(
'<div id="local-target"><p>2</p></div><span>1</span><portal></portal>'
);
});
test("with target in template (after portal)", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<span>1</span>
<Portal target="'#local-target'">
<p>2</p>
</Portal>
<div id="local-target"></div>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(parent.el!.innerHTML).toBe(
'<span>1</span><portal></portal><div id="local-target"><p>2</p></div>'
);
});
test("portal with target not in dom", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#does-not-exist'">
<div>2</div>
</Portal>
</div>`;
}
const parent = new Parent();
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe('Could not find any match for "#does-not-exist"');
expect(console.error).toBeCalledTimes(0);
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
console.error = consoleError;
});
test("portal with child and props", async () => {
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
mounted() {
steps.push("mounted");
expect(outside.innerHTML).toBe("<span>1</span>");
}
patched() {
steps.push("patched");
expect(outside.innerHTML).toBe("<span>2</span>");
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'">
<Child val="state.val"/>
</Portal>
</div>`;
state = useState({ val: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<span>1</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
parent.state.val = 2;
await nextTick();
expect(outside.innerHTML).toBe("<span>2</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
expect(steps).toEqual(["mounted", "patched"]);
});
test("portal with only text as content", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<t t-esc="'only text'"/>
</Portal>
</div>`;
}
const parent = new Parent();
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Portal must have exactly one non-text child (has 0)");
expect(console.error).toBeCalledTimes(0);
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
console.error = consoleError;
});
test("portal with no content", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<t t-if="false" t-esc="'ABC'"/>
</Portal>
</div>`;
}
const parent = new Parent();
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Portal must have exactly one non-text child (has 0)");
expect(console.error).toBeCalledTimes(0);
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
console.error = consoleError;
});
test("portal with many children", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<div>1</div>
<p>2</p>
</Portal>
</div>`;
}
const parent = new Parent();
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Portal must have exactly one non-text child (has 2)");
expect(console.error).toBeCalledTimes(0);
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
console.error = consoleError;
});
test("portal with dynamic body", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<span t-if="state.val" t-esc="state.val"/>
<div t-else=""/>
</Portal>
</div>`;
state = useState({ val: "ab" });
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe(`<span>ab</span>`);
parent.state.val = "";
await nextTick();
expect(outside.innerHTML).toBe(`<div></div>`);
});
test("portal could have dynamically no content", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<span t-if="state.val" t-esc="state.val"/>
</Portal>
</div>`;
state = { val: "ab" };
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe(`<span>ab</span>`);
let error;
try {
parent.state.val = "";
await parent.render();
} catch (e) {
error = e;
}
expect(outside.innerHTML).toBe(``);
expect(error).toBeDefined();
expect(error.message).toBe("Portal must have exactly one non-text child (has 0)");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("lifecycle hooks of portal sub component are properly called", async () => {
const steps: any[] = [];
class Child extends Component<any, any> {
static template = xml`<span t-esc="props.val"/>`;
mounted() {
steps.push("child:mounted");
}
willPatch() {
steps.push("child:willPatch");
}
patched() {
steps.push("child:patched");
}
willUnmount() {
steps.push("child:willUnmount");
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal t-if="state.hasChild" target="'#outside'">
<Child val="state.val"/>
</Portal>
</div>`;
state = useState({ hasChild: false, val: 1 });
mounted() {
steps.push("parent:mounted");
}
willPatch() {
steps.push("parent:willPatch");
}
patched() {
steps.push("parent:patched");
}
willUnmount() {
steps.push("parent:willUnmount");
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(steps).toEqual(["parent:mounted"]);
parent.state.hasChild = true;
await nextTick();
expect(steps).toEqual([
"parent:mounted",
"parent:willPatch",
"child:mounted",
"parent:patched"
]);
parent.state.val = 2;
await nextTick();
expect(steps).toEqual([
"parent:mounted",
"parent:willPatch",
"child:mounted",
"parent:patched",
"parent:willPatch",
"child:willPatch",
"child:patched",
"parent:patched"
]);
parent.state.hasChild = false;
await nextTick();
expect(steps).toEqual([
"parent:mounted",
"parent:willPatch",
"child:mounted",
"parent:patched",
"parent:willPatch",
"child:willPatch",
"child:patched",
"parent:patched",
"parent:willPatch",
"child:willUnmount",
"parent:patched"
]);
});
test("portal destroys on crash", async () => {
class Child extends Component<any, any> {
static template = xml`<span t-esc="props.error and this.will.crash" />`;
state = {};
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'" >
<Child error="state.error"/>
</Portal>
</div>`;
state = { error: false };
}
const parent = new Parent();
await parent.mount(fixture);
parent.state.error = true;
let error;
try {
await parent.render();
} catch (e) {
error = e;
}
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", () => {
test("events triggered on movable pure node are handled", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<span id="trigger-me" t-on-custom="_onCustom" t-esc="state.val"/>
</Portal>
</div>`;
state = useState({ val: "ab" });
_onCustom() {
this.state.val = "triggered";
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe(`<span id="trigger-me">ab</span>`);
outside.querySelector("#trigger-me")!.dispatchEvent(new Event("custom"));
await nextTick();
expect(outside.innerHTML).toBe(`<span id="trigger-me">triggered</span>`);
});
test("events triggered on movable owl components are redirected", async () => {
let childInst: Component<any, any> | null = null;
class Child extends Component<any, any> {
static template = xml`
<span t-on-custom="_onCustom" t-esc="props.val"/>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
_onCustom() {
this.trigger("custom-portal");
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div t-on-custom-portal="_onCustomPortal">
<Portal target="'#outside'">
<Child val="state.val"/>
</Portal>
</div>`;
state = useState({ val: "ab" });
_onCustomPortal() {
this.state.val = "triggered";
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe(`<span>ab</span>`);
childInst!.trigger("custom");
await nextTick();
expect(outside.innerHTML).toBe(`<span>triggered</span>`);
});
test("events triggered on contained movable owl components are redirected", async () => {
const steps: string[] = [];
let childInst: Component<any, any> | null = null;
class Child extends Component<any, any> {
static template = xml`
<span t-on-custom="_onCustom"/>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
_onCustom() {
this.trigger("custom-portal");
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div t-on-custom="_handled" t-on-custom-portal="_handled">
<Portal target="'#outside'">
<div>
<Child/>
</div>
</Portal>
</div>`;
_handled(ev) {
steps.push(ev.type);
}
}
const parent = new Parent();
await parent.mount(fixture);
childInst!.trigger("custom");
await nextTick();
// This is expected because trigger is synchronous
expect(steps).toMatchObject(["custom-portal", "custom"]);
});
test("Dom events are not mapped", async () => {
let childInst: Component<any, any> | null = null;
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`
<button>child</button>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div t-on-click="_handled">
<Portal target="'#outside'">
<Child />
</Portal>
</div>`;
_handled(ev) {
steps.push(ev.type as string);
}
}
const bodyListener = ev => {
steps.push(`body: ${ev.type}`);
};
document.body.addEventListener("click", bodyListener);
const parent = new Parent();
await parent.mount(fixture);
childInst!.el!.click();
expect(steps).toEqual(["body: click"]);
document.body.removeEventListener("click", bodyListener);
});
test("Nested portals event propagation", async () => {
const outside2 = document.createElement("div");
outside2.setAttribute("id", "outside2");
fixture.appendChild(outside2);
const steps: Array<string> = [];
let childInst: Component<any, any> | null = null;
class Child2 extends Component<any, any> {
static template = xml`<div>child2</div>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
}
class Child extends Component<any, any> {
static components = { Portal, Child2 };
static template = xml`
<Portal target="'#outside2'">
<Child2 />
</Portal>`;
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div t-on-custom='_handled'>
<Portal target="'#outside'">
<Child/>
</Portal>
</div>`;
_handled(ev) {
steps.push(`${ev.type} from ${ev.originalComponent.constructor.name}`);
}
}
const parent = new Parent();
await parent.mount(fixture);
childInst!.trigger("custom");
expect(steps).toEqual(["custom from Child2"]);
});
test("portal's parent's env is not polluted", async () => {
class Child extends Component<any, any> {
static template = xml`
<button>child</button>`;
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'">
<Child />
</Portal>
</div>`;
}
const parent = new Parent();
const parentEnv = Object.assign({}, parent.env);
await parent.mount(fixture);
expect(parentEnv).toStrictEqual(parent.env);
});
test("Portal composed with t-slot", async () => {
const steps: Array<string> = [];
let childInst: Component<any, any> | null = null;
class Child2 extends Component<any, any> {
static template = xml`<div>child2</div>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
}
class Child extends Component<any, any> {
static components = { Portal, Child2 };
static template = xml`
<Portal target="'#outside'">
<t t-slot="default"/>
</Portal>`;
}
class Parent extends Component<any, any> {
static components = { Child, Child2 };
static template = xml`
<div t-on-custom='_handled'>
<Child>
<Child2/>
</Child>
</div>`;
_handled(ev) {
steps.push(ev.type as string);
}
}
const parent = new Parent();
await parent.mount(fixture);
childInst!.trigger("custom");
expect(steps).toEqual(["custom"]);
});
});
describe("Portal: UI/UX", () => {
test("focus is kept across re-renders", async () => {
class Child extends Component<any, any> {
static template = xml`
<input id="target-me" t-att-placeholder="props.val"/>`;
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'">
<Child val="state.val"/>
</Portal>
</div>`;
state = useState({ val: "ab" });
}
const parent = new Parent();
await parent.mount(fixture);
const input = document.querySelector("#target-me");
expect(input!.nodeName).toBe("INPUT");
expect((input as HTMLInputElement).placeholder).toBe("ab");
(input as HTMLInputElement).focus();
expect(document.activeElement === input).toBeTruthy();
parent.state.val = "bc";
await nextTick();
const inputReRendered = document.querySelector("#target-me");
expect(inputReRendered!.nodeName).toBe("INPUT");
expect((inputReRendered as HTMLInputElement).placeholder).toBe("bc");
expect(document.activeElement === inputReRendered).toBeTruthy();
});
});
File diff suppressed because it is too large Load Diff
+11 -294
View File
@@ -46,16 +46,6 @@ describe("static templates", () => {
expect(renderToString(qweb, "test")).toBe("<div>word</div>"); 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", () => { test("div with a span child node", () => {
qweb.addTemplate("test", "<div><span>word</span></div>"); qweb.addTemplate("test", "<div><span>word</span></div>");
expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>"); expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>");
@@ -120,11 +110,9 @@ describe("t-esc", () => {
expect(renderToString(qweb, "test", { var: "ok" })).toBe("<span>ok</span>"); expect(renderToString(qweb, "test", { var: "ok" })).toBe("<span>ok</span>");
}); });
test("escaping", () => { test.skip("escaping", () => {
qweb.addTemplate("test", `<span><t t-esc="var"/></span>`); qweb.addTemplate("test", `<span><t t-esc="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<ok>abc</ok>" })).toBe( expect(renderToString(qweb, "test", { var: "<ok>" })).toBe("<span>&lt;ok&gt;</span>");
"<span>&amp;lt;ok&amp;gt;abc&amp;lt;/ok&amp;gt;</span>"
);
}); });
test("escaping on a node", () => { test("escaping on a node", () => {
@@ -141,49 +129,6 @@ describe("t-esc", () => {
qweb.addTemplate("test", `<span t-esc="var">nope</span>`); qweb.addTemplate("test", `<span t-esc="var">nope</span>`);
expect(renderToString(qweb, "test")).toBe("<span>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>");
});
test("div with falsy values", () => {
qweb.addTemplate(
"test",
`
<div>
<p t-esc="v1"/>
<p t-esc="v2"/>
<p t-esc="v3"/>
<p t-esc="v4"/>
<p t-esc="v5"/>
</div>`
);
const vals = {
v1: false,
v2: undefined,
v3: null,
v4: 0,
v5: ""
};
expect(renderToString(qweb, "test", vals)).toBe(
"<div><p>false</p><p></p><p></p><p>0</p><p></p></div>"
);
});
test("t-esc work with spread operator", () => {
qweb.addTemplate("test", `<span><t t-esc="[...state.list]"/></span>`);
const result = renderToString(qweb, "test", { state: { list: [1, 2] } });
expect(result).toBe("<span>1,2</span>");
});
}); });
describe("t-raw", () => { describe("t-raw", () => {
@@ -290,18 +235,6 @@ describe("t-set", () => {
); );
}); });
test("t-set with content and sub t-esc", () => {
qweb.addTemplate(
"test",
`<div>
<t t-set="setvar"><t t-esc="beep"/> boop</t>
<t t-esc="setvar"/>
</div>`
);
expect(renderToString(qweb, "test", { beep: "beep" })).toBe("<div>beep boop</div>");
});
test("evaluate value expression, part 2", () => { test("evaluate value expression, part 2", () => {
qweb.addTemplate( qweb.addTemplate(
"test", "test",
@@ -335,58 +268,6 @@ describe("t-set", () => {
expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>"); expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>");
expect(renderToString(qweb, "test", { flag: false })).toBe("<div>0</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", () => { describe("t-if", () => {
@@ -474,7 +355,7 @@ describe("t-if", () => {
}); });
test("t-esc with t-elif", () => { test("t-esc with t-elif", () => {
qweb.addTemplate("test", `<div><t t-if="false">abc</t><t t-else="" t-esc="'x'"/></div>`); qweb.addTemplate("test", `<div><t t-if="false">abc</t><t t-else="1" t-esc="'x'"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>x</div>"); expect(renderToString(qweb, "test")).toBe("<div>x</div>");
}); });
@@ -551,18 +432,6 @@ describe("attributes", () => {
expect(result).toBe(`<div foo="bar"></div>`); 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", () => { test("dynamic attribute with a dash", () => {
qweb.addTemplate("test", `<div t-att-data-action-id="id"/>`); qweb.addTemplate("test", `<div t-att-data-action-id="id"/>`);
const result = renderToString(qweb, "test", { id: 32 }); const result = renderToString(qweb, "test", { id: 32 });
@@ -666,7 +535,7 @@ describe("attributes", () => {
expect(result).toBe(`<div foo="a 0 is 1 of 2 ]"></div>`); expect(result).toBe(`<div foo="a 0 is 1 of 2 ]"></div>`);
}); });
test("various escapes", () => { test.skip("various escapes", () => {
// not needed?? // not needed??
qweb.addTemplate( qweb.addTemplate(
"test", "test",
@@ -682,7 +551,7 @@ describe("attributes", () => {
baz: 1, baz: 1,
qux: { qux: "<>" } qux: { qux: "<>" }
}); });
const expected = '<div foo="<foo" bar="0" baz="<1>" qux="<>"></div>'; const expected = `<div foo="&lt;foo" bar="&lt;bar&gt;" baz="&lt;&quot;&lt;baz&gt;&quot;&gt;" qux="&lt;&gt;"></div>`;
expect(result).toBe(expected); expect(result).toBe(expected);
}); });
@@ -707,19 +576,10 @@ describe("attributes", () => {
describe("t-call (template calling", () => { describe("t-call (template calling", () => {
test("basic caller", () => { 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("_basic-callee", "<div>ok</div>");
qweb.addTemplate("caller", '<t t-call="_basic-callee"/>'); qweb.addTemplate("caller", '<t t-call="_basic-callee"/>');
const expected = "<div>ok</div>"; const expected = "<div>ok</div>";
expect(renderToString(qweb, "caller")).toBe(expected); expect(renderToString(qweb, "caller")).toBe(expected);
expect(qweb.subTemplates["_basic-callee"].toString()).toMatchSnapshot();
}); });
test("t-call with t-if", () => { test("t-call with t-if", () => {
@@ -727,7 +587,6 @@ describe("t-call (template calling", () => {
qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>'); qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>');
const expected = "<div><span>ok</span></div>"; const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller", { flag: true })).toBe(expected); expect(renderToString(qweb, "caller", { flag: true })).toBe(expected);
expect(qweb.subTemplates["sub"].toString()).toMatchSnapshot();
}); });
test("t-call not allowed on a non t node", () => { test("t-call not allowed on a non t node", () => {
@@ -812,7 +671,6 @@ describe("t-call (template calling", () => {
`); `);
const expected = "<div><div><span>hey</span> <span>yay</span></div></div>"; const expected = "<div><div><span>hey</span> <span>yay</span></div></div>";
expect(renderToString(qweb, "main")).toBe(expected); expect(renderToString(qweb, "main")).toBe(expected);
expect(qweb.subTemplates["SubTemplate"].toString()).toMatchSnapshot();
}); });
test("cascading t-call t-raw='0'", () => { test("cascading t-call t-raw='0'", () => {
@@ -862,7 +720,7 @@ describe("t-call (template calling", () => {
`); `);
const expected = "<div><span>hey</span></div>"; const expected = "<div><span>hey</span></div>";
expect(renderToString(qweb, "recursive")).toBe(expected); expect(renderToString(qweb, "recursive")).toBe(expected);
const recursiveFn = Object.values(qweb.subTemplates)[0] as any; const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot(); expect(recursiveFn.toString()).toMatchSnapshot();
}); });
@@ -890,7 +748,7 @@ describe("t-call (template calling", () => {
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe( expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe(
expected expected
); );
const recursiveFn = Object.values(qweb.subTemplates)[0] as any; const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot(); expect(recursiveFn.toString()).toMatchSnapshot();
}); });
@@ -917,7 +775,7 @@ describe("t-call (template calling", () => {
const expected = const expected =
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>"; "<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); expect(renderToString(qweb, "Parent", { root }, { fiber: {} })).toBe(expected);
const recursiveFn = Object.values(qweb.subTemplates)[0] as any; const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot(); expect(recursiveFn.toString()).toMatchSnapshot();
}); });
@@ -927,68 +785,6 @@ describe("t-call (template calling", () => {
const expected = "<div><span>desk</span></div>"; const expected = "<div><span>desk</span></div>";
expect(trim(renderToString(qweb, "abcd"))).toBe(expected); expect(trim(renderToString(qweb, "abcd"))).toBe(expected);
}); });
test("t-call, conditional and t-set in t-call body", () => {
QWeb.registerTemplate("callee1", "<div>callee1</div>");
QWeb.registerTemplate("callee2", '<div>callee2 <t t-esc="v"/></div>');
QWeb.registerTemplate(
"caller",
`<div>
<t t-set="v1" t-value="'elif'"/>
<t t-if="v1 === 'if'" t-call="callee1" />
<t t-elif="v1 === 'elif'" t-call="callee2" >
<t t-set="v" t-value="'success'" />
</t>
</div>`
);
const rendered = renderToString(qweb, "caller");
expect(rendered).toBe(`<div><div>callee2 success</div></div>`);
});
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", () => { describe("foreach", () => {
@@ -1057,9 +853,9 @@ describe("foreach", () => {
<t t-foreach="[1]" t-as="item"><t t-esc="item"/></t> <t t-foreach="[1]" t-as="item"><t t-esc="item"/></t>
</div>` </div>`
); );
const context = { __owl__: {} }; const context = {};
renderToString(qweb, "test", context); renderToString(qweb, "test", context);
expect(Object.keys(context)).toEqual(["__owl__"]); expect(Object.keys(context).length).toBe(0);
}); });
test("t-foreach in t-forach", () => { test("t-foreach in t-forach", () => {
@@ -1337,69 +1133,6 @@ describe("t-on", () => {
expect(owner.state.counter).toBe(2); expect(owner.state.counter).toBe(2);
}); });
test("t-on with inline statement, part 2", () => {
qweb.addTemplate("test", `<button t-on-click="state.flag = !state.flag">Toggle</button>`);
let owner = {
state: {
flag: true
}
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.flag).toBe(true);
(<HTMLElement>node).click();
expect(owner.state.flag).toBe(false);
(<HTMLElement>node).click();
expect(owner.state.flag).toBe(true);
});
test("t-on with inline statement, part 3", () => {
qweb.addTemplate("test", `<button t-on-click="state.n = someFunction(3)">Toggle</button>`);
let owner = {
someFunction(n) {
return n + 1;
},
state: {
n: 11
}
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.n).toBe(11);
(<HTMLElement>node).click();
expect(owner.state.n).toBe(4);
});
test("t-on with t-call", async () => {
expect.assertions(2);
qweb.addTemplate("sub", `<p t-on-click="update">lucas</p>`);
qweb.addTemplate("main", `<div><t t-call="sub"/></div>`);
let owner = {
update() {
expect(this).toBe(owner);
}
};
const node = renderToDOM(qweb, "main", owner, { handlers: [] });
(<HTMLElement>node).querySelector("p")!.click();
});
test("t-on, with arguments and t-call", async () => {
expect.assertions(3);
qweb.addTemplate("sub", `<p t-on-click="update(value)">lucas</p>`);
qweb.addTemplate("main", `<div><t t-call="sub"/></div>`);
let owner = {
update(val) {
expect(this).toBe(owner);
expect(val).toBe(444);
},
value: 444
};
const node = renderToDOM(qweb, "main", owner, { handlers: [] });
(<HTMLElement>node).querySelector("p")!.click();
});
test("t-on with prevent and/or stop modifiers", async () => { test("t-on with prevent and/or stop modifiers", async () => {
expect.assertions(7); expect.assertions(7);
qweb.addTemplate( qweb.addTemplate(
@@ -1772,7 +1505,7 @@ describe("debugging", () => {
console.log = jest.fn(); console.log = jest.fn();
qweb.addTemplate( qweb.addTemplate(
"test", "test",
`<div t-debug=""><t t-if="true"><span t-debug="">hey</span></t></div>` `<div t-debug="1"><t t-if="true"><span t-debug="1">hey</span></t></div>`
); );
qweb.render("test"); qweb.render("test");
expect(qweb.templates.test.fn.toString()).toMatchSnapshot(); expect(qweb.templates.test.fn.toString()).toMatchSnapshot();
@@ -1781,22 +1514,6 @@ describe("debugging", () => {
console.log = consoleLog; 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="">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", () => { test("t-log", () => {
const consoleLog = console.log; const consoleLog = console.log;
console.log = jest.fn(); console.log = jest.fn();
+29 -55
View File
@@ -50,12 +50,6 @@ describe("tokenizer", () => {
{ type: "OPERATOR", value: "typeof " }, { type: "OPERATOR", value: "typeof " },
{ type: "SYMBOL", value: "a" } { type: "SYMBOL", value: "a" }
]); ]);
expect(tokenize("a...1")).toEqual([
{ type: "SYMBOL", value: "a" },
{ type: "OPERATOR", value: "..." },
{ type: "VALUE", value: "1" }
]);
}); });
test("strings", () => { test("strings", () => {
@@ -98,7 +92,7 @@ describe("expression evaluation", () => {
test("parenthesis", () => { test("parenthesis", () => {
expect(compileExpr("(1)", {})).toBe("(1)"); expect(compileExpr("(1)", {})).toBe("(1)");
expect(compileExpr("a*(1 +3)", {})).toBe("scope['a']*(1+3)"); expect(compileExpr("a*(1 +3)", {})).toBe("context['a']*(1+3)");
}); });
test("objects and sub objects", () => { test("objects and sub objects", () => {
@@ -106,8 +100,8 @@ describe("expression evaluation", () => {
}); });
test("replacing variables", () => { test("replacing variables", () => {
expect(compileExpr("a", {})).toBe("scope['a']"); expect(compileExpr("a", {})).toBe("context['a']");
expect(compileExpr("a", { a: { id: "_3", expr: "scope._3" } })).toBe("scope._3"); expect(compileExpr("a", { a: { id: "_3", expr: "" } })).toBe("_3");
}); });
test("arrays and objects", () => { test("arrays and objects", () => {
@@ -117,76 +111,56 @@ describe("expression evaluation", () => {
}); });
test("dot operator", () => { test("dot operator", () => {
expect(compileExpr("a.b", {})).toBe("scope['a'].b"); expect(compileExpr("a.b", {})).toBe("context['a'].b");
expect(compileExpr("a.b.c", {})).toBe("scope['a'].b.c"); expect(compileExpr("a.b.c", {})).toBe("context['a'].b.c");
}); });
test("various unary operators", () => { test("various unary operators", () => {
expect(compileExpr("!flag", {})).toBe("!scope['flag']"); expect(compileExpr("!flag", {})).toBe("!context['flag']");
expect(compileExpr("-3", {})).toBe("-3"); expect(compileExpr("-3", {})).toBe("-3");
expect(compileExpr("-a", {})).toBe("-scope['a']"); expect(compileExpr("-a", {})).toBe("-context['a']");
expect(compileExpr("typeof a", {})).toBe("typeof scope['a']"); expect(compileExpr("typeof a", {})).toBe("typeof context['a']");
}); });
test("various binary operators", () => { test("various binary operators", () => {
expect(compileExpr("color == 'black'", {})).toBe("scope['color']=='black'"); expect(compileExpr("color == 'black'", {})).toBe("context['color']=='black'");
expect(compileExpr("a || b", {})).toBe("scope['a']||scope['b']"); expect(compileExpr("a || b", {})).toBe("context['a']||context['b']");
expect(compileExpr("color === 'black'", {})).toBe("scope['color']==='black'"); expect(compileExpr("color === 'black'", {})).toBe("context['color']==='black'");
expect(compileExpr("'li_'+item", {})).toBe("'li_'+scope['item']"); expect(compileExpr("'li_'+item", {})).toBe("'li_'+context['item']");
expect(compileExpr("state.val > 1", {})).toBe("scope['state'].val>1"); expect(compileExpr("state.val > 1", {})).toBe("context['state'].val>1");
}); });
test("boolean operations", () => { test("boolean operations", () => {
expect(compileExpr("a && b", {})).toBe("scope['a']&&scope['b']"); expect(compileExpr("a && b", {})).toBe("context['a']&&context['b']");
}); });
test("ternary operators", () => { test("ternary operators", () => {
expect(compileExpr("a ? b: '2'", {})).toBe("scope['a']?scope['b']:'2'"); expect(compileExpr("a ? b: '2'", {})).toBe("context['a']?context['b']:'2'");
expect(compileExpr("a ? b: (c or '2') ", {})).toBe("scope['a']?scope['b']:(scope['c']||'2')"); expect(compileExpr("a ? b: (c or '2') ", {})).toBe(
"context['a']?context['b']:(context['c']||'2')"
);
expect(compileExpr("a ? {test:c}: [1,u]", {})).toBe( expect(compileExpr("a ? {test:c}: [1,u]", {})).toBe(
"scope['a']?{test:scope['c']}:[1,scope['u']]" "context['a']?{test:context['c']}:[1,context['u']]"
); );
}); });
test("word replacement", () => { test("word replacement", () => {
expect(compileExpr("a or b", {})).toBe("scope['a']||scope['b']"); expect(compileExpr("a or b", {})).toBe("context['a']||context['b']");
expect(compileExpr("a and b", {})).toBe("scope['a']&&scope['b']"); expect(compileExpr("a and b", {})).toBe("context['a']&&context['b']");
}); });
test("function calls", () => { test("function calls", () => {
expect(compileExpr("a()", {})).toBe("scope['a']()"); expect(compileExpr("a()", {})).toBe("context['a']()");
expect(compileExpr("a(1)", {})).toBe("scope['a'](1)"); expect(compileExpr("a(1)", {})).toBe("context['a'](1)");
expect(compileExpr("a(1,2)", {})).toBe("scope['a'](1,2)"); expect(compileExpr("a(1,2)", {})).toBe("context['a'](1,2)");
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe("scope['a'](1,2,{a:[scope['a']]})"); expect(compileExpr("a(1,2,{a:[a]})", {})).toBe("context['a'](1,2,{a:[context['a']]})");
expect(compileExpr("'x'.toUpperCase()", {})).toBe("'x'.toUpperCase()"); expect(compileExpr("'x'.toUpperCase()", {})).toBe("'x'.toUpperCase()");
expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe("'x'.toUpperCase({a:3})"); expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe("'x'.toUpperCase({a:3})");
expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "scope._v5" } })).toBe( expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })).toBe(
"'x'.toUpperCase(scope._v5)" "'x'.toUpperCase(_v5)"
); );
expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "scope._v5" } })).toBe( expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })).toBe(
"'x'.toUpperCase({b:scope._v5})" "'x'.toUpperCase({b:_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)"
);
});
test("assignation", () => {
expect(compileExpr("a = b", {})).toBe("scope['a']=scope['b']");
expect(compileExpr("a += b", {})).toBe("scope['a']+=scope['b']");
expect(compileExpr("a -= b", {})).toBe("scope['a']-=scope['b']");
expect(compileExpr("a.b = !a.b", {})).toBe("scope['a'].b=!scope['a'].b");
});
test("spread operator", () => {
expect(compileExpr("[...state.list]", {})).toBe("[...scope['state'].list]");
expect(compileExpr("f(...state.list)", {})).toBe("scope['f'](...scope['state'].list)");
expect(compileExpr("f([...list])", {})).toBe("scope['f']([...scope['list']])");
});
}); });
+13 -14
View File
@@ -1,22 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Link component can render simple cases 1`] = ` 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 utils = this.constructor.utils;
let scope = Object.create(context); let owner = context;
let h = this.h; var h = this.h;
let _4 = utils.toObj({'router-link-active':scope['isActive']}); let _6 = utils.toObj({'router-link-active':context['isActive']});
let _5 = scope['href']; var _7 = context['href'];
let c6 = [], p6 = {key:6,attrs:{href: _5},class:_4,on:{}}; let c8 = [], p8 = {key:8,attrs:{href: _7},class:_6,on:{}};
let vn6 = h('a', p6, c6); var vn8 = h('a', p8, c8);
extra.handlers['click__7__'] = extra.handlers['click__7__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['navigate'](e);}; extra.handlers['click' + 8] = extra.handlers['click' + 8] || function (e) {const fn = context['navigate'];if (fn) { fn.call(owner, e); } else { context.navigate; }};
p6.on['click'] = extra.handlers['click__7__']; p8.on['click'] = extra.handlers['click' + 8];
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot9 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot8) { if (slot9) {
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c6, parent: extra.parent || context})); slot9.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c8, vars: extra.vars, parent: extra.parent || owner}));
} }
return vn6; return vn8;
}" }"
`; `;
@@ -1,45 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RouteComponent can render simple cases 1`] = ` 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 utils = this.constructor.utils;
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let scope = Object.create(context); let owner = context;
let result; let result;
let h = this.h; var h = this.h;
if (scope['routeComponent']) { if (context['routeComponent']) {
{ const nodeKey6 = context['env'].router.currentRouteName;
let key0 = scope['env'].router.currentRouteName; //COMPONENT
// Component 'routeComponent' let k9 = \`__10__\` + nodeKey6;
let k5 = \`__5__\${key0}__\`; let w8 = k9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k9]] : false;
let w4 = k5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k5]] : false; let vn11 = {};
let vn6 = {}; result = vn11;
result = vn6; let props8 = Object.assign({}, context['env'].router.currentParams);
let props4 = Object.assign({}, scope['env'].router.currentParams); if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) {
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) { w8.destroy();
w4.destroy(); w8 = false;
w4 = false;
}
if (w4) {
w4.__updateProps(props4, extra.fiber, undefined);
let pvnode = w4.__owl__.pvnode;
utils.defineProxy(vn6, pvnode);
} else {
let componentKey4 = \`routeComponent\`;
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| scope['routeComponent'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[k5] = w4.__owl__.id;
let fiber = w4.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k5, hook: {remove() {},destroy(vn) {w4.destroy();}}});
utils.defineProxy(vn6, pvnode);
w4.__owl__.pvnode = pvnode;
}
w4.__owl__.parentLastFiberId = extra.fiber.id;
} }
if (w8) {
w8.__updateProps(props8, extra.fiber, undefined, undefined);
let pvnode = w8.__owl__.pvnode;
utils.defineProxy(vn11, pvnode);
} else {
let componentKey8 = \`routeComponent\`;
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| context['routeComponent'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
w8 = new W8(parent, props8);
parent.__owl__.cmap[k9] = w8.__owl__.id;
let def7 = w8.__prepare(extra.fiber, undefined, undefined);
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
const fiber = w8.__owl__.currentFiber;
def7.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
utils.defineProxy(vn11, pvnode);
w8.__owl__.pvnode = pvnode;
}
w8.__owl__.parentLastFiberId = extra.fiber.id;
} }
return result; return result;
}" }"
-32
View File
@@ -84,38 +84,6 @@ describe("connecting a component to store", () => {
expect(fixture.innerHTML).toBe("<div><span>ok</span></div>"); expect(fixture.innerHTML).toBe("<div><span>ok</span></div>");
}); });
test("map works on the result of useStore when the resulting array changes for a bigger one", async () => {
const state = { smallerArray: [1], biggerArray: [2, 3], useSmallArray: true };
const store = new Store({ state });
class App extends Component<any, any> {
static template = xml`<div t-esc="mapAdd"/>`;
storeProps = {
array: useStore(state => {
if (state.useSmallArray) {
return state.smallerArray;
}
return state.biggerArray;
})
};
get mapAdd() {
return this.storeProps.array.map(a => {
return a + 1;
});
}
}
(<any>env).store = store;
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>2</div>");
store.state.useSmallArray = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div>3,4</div>");
});
test("throw error if no store is found", async () => { test("throw error if no store is found", async () => {
class App extends Component<any, any> { class App extends Component<any, any> {
static template = xml`<div></div>`; static template = xml`<div></div>`;
-57
View File
@@ -1,57 +0,0 @@
/**
* 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 { useState } from "../../src/hooks";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
let fixture: HTMLElement = makeTestFixture();
let env: Env = makeTestEnv();
Component.env = env;
debugOwl(owl, {});
test("can log full lifecycle", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
class Child extends Component<any, any> {
static template = xml`<div>child</div>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Child t-if="state.flag"/></div>`;
static components = { Child };
state = useState({ flag: false });
}
const parent = new Parent(null, {});
await parent.mount(fixture);
parent.state.flag = true;
await nextTick();
expect(steps).toEqual([
"[OWL_DEBUG] Parent<id=1> constructor, props={}",
"[OWL_DEBUG] Parent<id=1> mount",
"[OWL_DEBUG] Parent<id=1> willStart",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] Parent<id=1> render",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] Child<id=2> constructor, props={}",
"[OWL_DEBUG] Child<id=2> willStart",
"[OWL_DEBUG] Child<id=2> rendering template",
"[OWL_DEBUG] Parent<id=1> willPatch",
"[OWL_DEBUG] Child<id=2> mounted",
"[OWL_DEBUG] Parent<id=1> patched"
]);
console.log = log;
});
-50
View File
@@ -1,50 +0,0 @@
/**
* 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("can log scheduler start and stop", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
class Child extends Component<any, any> {
static template = xml`<div>child</div>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Child /></div>`;
static components = { Child };
}
const parent = new Parent(null, {});
await parent.mount(fixture);
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] Child<id=2> constructor, props={}",
"[OWL_DEBUG] Child<id=2> willStart",
"[OWL_DEBUG] Child<id=2> rendering template",
"[OWL_DEBUG] Child<id=2> mounted",
"[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue"
]);
console.log = log;
});
-46
View File
@@ -1,46 +0,0 @@
/**
* We can only make one test per file, since the debug tool modify in place
* the owl object in a way that is difficult to undo.
*/
import { debugOwl } from "../../tools/debug";
import * as owl from "../../src/index";
import { Component, Env } from "../../src/component/component";
import { xml } from "../../src/tags";
import { makeTestFixture, makeTestEnv } from "../helpers";
let fixture: HTMLElement = makeTestFixture();
let env: Env = makeTestEnv();
Component.env = env;
debugOwl(owl, { logScheduler: true });
test("log a specific message for render method calls if component is not mounted", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
class Parent extends Component<any, any> {
static template = xml`<div><t t-esc="state.value"/></div>`;
state = owl.hooks.useState({ value: 1 });
}
const parent = new Parent(null, {});
await parent.mount(fixture);
parent.unmount();
parent.state.value = 2;
expect(steps).toEqual([
"[OWL_DEBUG] Parent<id=1> constructor, props={}",
"[OWL_DEBUG] Parent<id=1> mount",
"[OWL_DEBUG] Parent<id=1> willStart",
"[OWL_DEBUG] scheduler: start running tasks queue",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue",
"[OWL_DEBUG] Parent<id=1> willUnmount",
"[OWL_DEBUG] Parent<id=1> render (warning: component is not mounted, this render has no effect)"
]);
console.log = log;
});
+173
View File
@@ -0,0 +1,173 @@
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();
@@ -2,11 +2,12 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>OWL v1.0.0-beta1 Benchmark</title> <title>OWL 0.10.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/> <link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script> <script src='owl.js'></script>
</head> </head>
<body> <body>
<div id='main'></div>
<script src='app.js' type="module"></script> <script src='app.js' type="module"></script>
</body> </body>
</html> </html>
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="'log'"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+170
View File
@@ -0,0 +1,170 @@
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();
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.12.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
<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,11 +1,10 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js"; import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Likes Counter Widget // Likes Counter Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class Counter extends owl.Component { class Counter extends owl.Component {
state = useState({ counter: 0 }); state = { counter: 0 };
increment() { increment() {
this.state.counter++; this.state.counter++;
@@ -16,6 +15,8 @@ class Counter extends owl.Component {
// Message Widget // Message Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class Message extends owl.Component { class Message extends owl.Component {
widgets = { Counter };
shouldUpdate(nextProps) { shouldUpdate(nextProps) {
return nextProps.message !== this.props.message; return nextProps.message !== this.props.message;
} }
@@ -25,14 +26,13 @@ class Message extends owl.Component {
}); });
} }
} }
Message.components = { Counter };
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Root Widget // Root Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class App extends owl.Component { class App extends owl.Component {
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false }); widgets = { Message };
logRef = useRef("log"); state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() { mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`); this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
@@ -108,10 +108,10 @@ class App extends owl.Component {
updateSomeMessages() { updateSomeMessages() {
this.benchmark("update every 10th", () => { this.benchmark("update every 10th", () => {
const messages = this.state.messages; const messages = this.state.messages;
for (let i = 0; i < messages.length; i += 10) { for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]); const msg = Object.assign({}, messages[i]);
msg.author += "!!!"; msg.author += "!!!";
messages[i] = msg; this.set(messages, i, msg);
} }
}); });
} }
@@ -129,25 +129,32 @@ class App extends owl.Component {
div.classList.add("bold"); div.classList.add("bold");
} }
div.textContent = `> ${str}`; div.textContent = `> ${str}`;
this.logRef.el.appendChild(div); this.refs.log.appendChild(div);
this.logRef.el.scrollTop = this.logRef.el.scrollHeight; this.refs.log.scrollTop = this.refs.log.scrollHeight;
} }
clearLog() { clearLog() {
this.logRef.el.innerHTML = ""; this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
} }
} }
App.components = { Message };
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Application initialization // Application initialization
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
async function start() { async function start() {
const templates = await owl.utils.loadFile("templates.xml"); const templates = await owl.utils.loadTemplates("templates.xml");
App.env = { const env = {
qweb: new owl.QWeb({ templates }) qweb: new owl.QWeb(templates)
}; };
const app = new App(); const app = new App(env);
app.mount(document.body); app.mount(document.body);
} }
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.14.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+161
View File
@@ -0,0 +1,161 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
+13
View File
@@ -0,0 +1,13 @@
<!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>
File diff suppressed because it is too large Load Diff
@@ -12,11 +12,11 @@
</div> </div>
<div class="flags"> <div class="flags">
<div> <div>
<input type="checkbox" id="multipleflag" t-model="state.multipleFlag"/> <input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label> <label for="multipleflag">Do it 20x</label>
</div> </div>
<div> <div>
<input type="checkbox" id="clearFlag" t-model="state.clearAfterFlag" /> <input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label> <label for="clearFlag">Clear after</label>
</div> </div>
</div> </div>
+173
View File
@@ -0,0 +1,173 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
template = "Counter";
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
template = "Message";
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
template = "App";
mounted() {
this.log(
`Benchmarking Owl v${owl._version} (build date: ${
owl._date
})`
);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(data) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.8.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl-0.8.0.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
<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>
+38 -50
View File
@@ -5,7 +5,15 @@
* to log lot of helpful information on how Owl components behave. * to log lot of helpful information on how Owl components behave.
*/ */
function debugOwl(owl, options) { let debugSetup = {
// componentBlackList: /App/, // regexp
// componentWhiteList: /SomeComponent/, // regexp
// methodBlackList: ["mounted"], // list of method names
// methodWhiteList: ["willStart"], // list of method names
logScheduler: true, // display/mute scheduler logs
logStore: true // display/mute store logs
};
{
let prefix = "[OWL_DEBUG]"; let prefix = "[OWL_DEBUG]";
let current; let current;
Object.defineProperty(owl.Component, "current", { Object.defineProperty(owl.Component, "current", {
@@ -15,10 +23,10 @@
set(comp) { set(comp) {
current = comp; current = comp;
const name = comp.constructor.name; const name = comp.constructor.name;
if (options.componentBlackList && options.componentBlackList.test(name)) { if (debugSetup.componentBlackList && debugSetup.componentBlackList.test(name)) {
return; return;
} }
if (options.componentWhiteList && !options.componentWhiteList.test(name)) { if (debugSetup.componentWhiteList && !debugSetup.componentWhiteList.test(name)) {
return; return;
} }
let __owl__; let __owl__;
@@ -44,88 +52,82 @@
function debugComponent(component, name, id) { function debugComponent(component, name, id) {
let fullName = `${name}<id=${id}>`; let fullName = `${name}<id=${id}>`;
let log = str => console.log(`${prefix} ${fullName} ${str}`);
let shouldDebug = method => { let shouldDebug = method => {
if (options.methodBlackList && options.methodBlackList.includes(method)) { if (debugSetup.methodBlackList && debugSetup.methodBlackList.includes(method)) {
return false; return false;
} }
if (options.methodWhiteList && !options.methodWhiteList.includes(method)) { if (debugSetup.methodWhiteList && !debugSetup.methodWhiteList.includes(method)) {
return false; return false;
} }
return true; return true;
}; };
if (shouldDebug("constructor")) { if (shouldDebug("constructor")) {
log(`constructor, props=${toStr(component.props)}`); console.log(`${prefix} ${fullName} constructor, props=${toStr(component.props)}`);
} }
if (shouldDebug("willStart")) { if (shouldDebug("willStart")) {
owl.hooks.onWillStart(() => { owl.hooks.onWillStart(() => {
log(`willStart`); console.log(`${prefix} ${fullName} willStart`);
}); });
} }
if (shouldDebug("mounted")) { if (shouldDebug("mounted")) {
owl.hooks.onMounted(() => { owl.hooks.onMounted(() => {
log(`mounted`); console.log(`${prefix} ${fullName} mounted`);
}); });
} }
if (shouldDebug("willUpdateProps")) { if (shouldDebug("willUpdateProps")) {
owl.hooks.onWillUpdateProps(nextProps => { owl.hooks.onWillUpdateProps(nextProps => {
log(`willUpdateProps, nextprops=${toStr(nextProps)}`); console.log(`${prefix} ${fullName} willUpdateProps, nextprops=${toStr(nextProps)}`);
}); });
} }
if (shouldDebug("willPatch")) { if (shouldDebug("willPatch")) {
owl.hooks.onWillPatch(() => { owl.hooks.onWillPatch(() => {
log(`willPatch`); console.log(`${prefix} ${fullName} willPatch`);
}); });
} }
if (shouldDebug("patched")) { if (shouldDebug("patched")) {
owl.hooks.onPatched(() => { owl.hooks.onPatched(() => {
log(`patched`); console.log(`${prefix} ${fullName} patched`);
}); });
} }
if (shouldDebug("willUnmount")) { if (shouldDebug("willUnmount")) {
owl.hooks.onWillUnmount(() => { owl.hooks.onWillUnmount(() => {
log(`willUnmount`); console.log(`${prefix} ${fullName} willUnmount`);
}); });
} }
const __render = component.__render.bind(component); const __render = component.__render.bind(component);
component.__render = function(...args) { component.__render = function(...args) {
log(`rendering template`); console.log(`${prefix} ${fullName} rendering template`);
__render(...args); __render(...args);
}; };
const render = component.render.bind(component); const render = component.render.bind(component);
component.render = function(...args) { component.render = function(...args) {
const __owl__ = component.__owl__; console.log(`${prefix} ${fullName} render`);
let msg = `render`;
if (!__owl__.isMounted && !__owl__.currentFiber) {
msg += ` (warning: component is not mounted, this render has no effect)`;
}
log(msg);
return render(...args); return render(...args);
}; };
const mount = component.mount.bind(component); const mount = component.mount.bind(component);
component.mount = function(...args) { component.mount = function(...args) {
log(`mount`); console.log(`${prefix} ${fullName} mount`);
return mount(...args); return mount(...args);
}; };
} }
if (options.logScheduler) { if (debugSetup.logScheduler) {
let start = owl.Component.scheduler.start; let isRunning;
let stop = owl.Component.scheduler.stop; Object.defineProperty(owl.Component.scheduler, "isRunning", {
owl.Component.scheduler.start = function () { get() {
if (!this.isRunning) { return isRunning;
console.log(`${prefix} scheduler: start running tasks queue`); },
set(val) {
if (val) {
console.log(`${prefix} scheduler: start running tasks queue`);
} else {
console.log(`${prefix} scheduler: stop running tasks queue`);
}
isRunning = val;
} }
start.call(this); });
};
owl.Component.scheduler.stop = function () {
if (this.isRunning) {
console.log(`${prefix} scheduler: stop running tasks queue`);
}
stop.call(this);
};
} }
if (options.logStore) { if (debugSetup.logStore) {
let dispatch = owl.Store.prototype.dispatch; let dispatch = owl.Store.prototype.dispatch;
owl.Store.prototype.dispatch = function(action, ...payload) { owl.Store.prototype.dispatch = function(action, ...payload) {
console.log(`${prefix} store: action '${action}' dispatched. Payload: '${toStr(payload)}'`); console.log(`${prefix} store: action '${action}' dispatched. Payload: '${toStr(payload)}'`);
@@ -133,17 +135,3 @@
}; };
} }
} }
// This debug function can then be used like this:
//
// debugOwl(owl, {
// componentBlackList: /App/, // regexp
// componentWhiteList: /SomeComponent/, // regexp
// methodBlackList: ["mounted"], // list of method names
// methodWhiteList: ["willStart"], // list of method names
// logScheduler: true, // display/mute scheduler logs
// logStore: true // display/mute store logs
// });
module.exports.debugOwl = debugOwl;
+5 -1
View File
@@ -28,15 +28,19 @@
<div class="benchmarks"> <div class="benchmarks">
<ul> <ul>
<li><a href="benchmarks/owl-0.7.0">OWL 0.7.0</a></li> <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.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.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.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.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.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.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.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-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> <li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul> </ul>
<ul> <ul>
+3 -114
View File
@@ -1101,7 +1101,7 @@ const RESPONSIVE_XML = `<templates>
<t t-raw="maincontent"/> <t t-raw="maincontent"/>
</div> </div>
</div> </div>
<t t-else=""> <t t-else="1">
<t t-raw="maincontent"/> <t t-raw="maincontent"/>
</t> </t>
</div> </div>
@@ -1438,117 +1438,12 @@ const FORM_XML = `<templates>
<div>Text: <t t-esc="state.text"/></div> <div>Text: <t t-esc="state.text"/></div>
<div>Other Text: <t t-esc="state.othertext"/></div> <div>Other Text: <t t-esc="state.othertext"/></div>
<div>Number: <t t-esc="state.number"/></div> <div>Number: <t t-esc="state.number"/></div>
<div>Boolean: <t t-if="state.bool">True</t><t t-else="">False</t></div> <div>Boolean: <t t-if="state.bool">True</t><t t-else="1">False</t></div>
<div>Color: <t t-esc="state.color"/></div> <div>Color: <t t-esc="state.color"/></div>
</div> </div>
</templates> </templates>
`; `;
const PORTAL_COMPONENTS = `
// This shows the expected use case of Portal
// which is to implement something similar
// to bootstrap modal
const { Component, useState } = owl;
const { Portal } = owl.misc;
class Modal extends Component {}
Modal.components = { Portal };
class Dialog extends Component {}
Dialog.components = { Modal };
class Interstellar extends Component {}
// Main root component
class App extends Component {
state = useState({
name: 'Portal used for Dialog (Modal)',
dialog: false,
text: 'Hello !',
});
}
App.components = { Dialog , Interstellar };
// Application setup
const app = new App();
app.mount(document.body);
`;
const PORTAL_XML = `
<templates>
<t t-name="Modal">
<Portal target="'body'">
<div class="owl-modal-supercontainer">
<div class="owl-modal-backdrop"></div>
<div class="owl-modal-container">
<t t-slot="default" />
</div>
</div>
</Portal>
</t>
<t t-name="Dialog">
<Modal>
<div class="owl-dialog-body">
<t t-slot="default" />
</div>
</Modal>
</t>
<div t-name="Interstellar" class="owl-interstellar">
<h4>This is a subComponent</h4>
<p>The events it triggers will go through the Portal and be teleported
on the other side of the wormhole it has created</p>
<button t-on-click="trigger('collapse-all')">Close the wormhole</button>
</div>
<div t-name="App" t-on-collapse-all="state.dialog=false">
<div t-esc="state.name"/>
<button t-on-click="state.dialog = true">Open Dialog</button>
<Dialog t-if="state.dialog">
<div t-esc="state.text"/>
<Interstellar />
</Dialog>
</div>
</templates>
`;
const PORTAL_CSS = `
.owl-modal-supercontainer {
position: static;
}
.owl-modal-backdrop {
position: fixed;
top: 0;
left:0;
background-color: #000000;
opacity: 0.5;
width: 100vw;
height: 100vh;
z-index: 1000;
}
.owl-modal-container {
opacity:1;
z-index: 1050;
position: fixed;
top: 0;
left:0;
width: 100%;
height: 100%;
}
.owl-dialog-body {
max-width: 500px;
margin: 0 auto;
position: relative;
text-align: center;
padding: 2rem;
background-color: #FFFFFF;
max-height: 100%;
}
.owl-interstellar {
border: groove;
}`
const WMS = `// This example is slightly more complex than usual. We demonstrate const WMS = `// This example is slightly more complex than usual. We demonstrate
// here a way to manage sub windows in Owl, declaratively. This is still just a // here a way to manage sub windows in Owl, declaratively. This is still just a
// demonstration. Managing windows can be as complex as we want. For example, // demonstration. Managing windows can be as complex as we want. For example,
@@ -1875,11 +1770,5 @@ export const SAMPLES = [
code: ASYNC_COMPONENTS, code: ASYNC_COMPONENTS,
xml: ASYNC_COMPONENTS_XML, xml: ASYNC_COMPONENTS_XML,
css: ASYNC_COMPONENTS_CSS css: ASYNC_COMPONENTS_CSS
}, }
{
description: "Portal (Dialog)",
code: PORTAL_COMPONENTS,
xml: PORTAL_XML,
css: PORTAL_CSS,
},
]; ];