Compare commits

..

47 Commits

Author SHA1 Message Date
Géry Debongnie e402ee62de [REL] v1.0.4 2020-01-21 10:28:50 +01:00
Géry Debongnie 5711eb065d [ADD] tooling: add release script
More work is needed to complete this, but the basics is here, and it is
difficult to test.

closes #612
2020-01-21 10:10:45 +01:00
Géry Debongnie 6739e79aac [ADD] tools: add single file component example to playground 2020-01-21 09:27:30 +01:00
Géry Debongnie 1c8dc97fca [FIX] component: can mount on different target without unmounting
closes #616
2020-01-21 09:26:38 +01:00
Géry Debongnie f9cac94dc7 [REF] tests: split component.test into multiple files 2020-01-21 09:26:38 +01:00
Géry Debongnie c62c3fea19 [FIX] types: improve type of useExternalListener
so it can work on window

closes #613
2020-01-21 08:11:40 +01:00
Géry Debongnie 3845607feb [DOC] store: clarify how a component can find a store
closes #617
2020-01-21 08:08:14 +01:00
Géry Debongnie 3fcbb9ad16 [REL] v1.0.3 2020-01-09 16:27:18 +01:00
Géry Debongnie e04b160bff [REL] v1.0.2
v1.0.2 because I am fighting with npm...
2020-01-09 15:12:32 +01:00
Géry Debongnie bc234d2053 [ADD] tools: add benchmarks for 1.0.0 2020-01-09 14:52:27 +01:00
Géry Debongnie 3d0f046d9a [REL] v1.0.0
Finally!!
2020-01-09 14:37:36 +01:00
Géry Debongnie 4c75bae0e8 [IMP] owl: improve npm packaging
closes #611
2020-01-09 14:35:13 +01:00
Géry Debongnie 54f5819ef9 [ADD] hooks: add useExternalListener hook
It is very useful.

Also, this commit prettifies the code.

closes #608
2020-01-09 14:06:30 +01:00
Joseph Caburnay 0c9c5b877b [FIX] playground: not all completed is deleted
Before this fix, not all completed tasks are deleted when clicking clear
completed button because we are looping to a mutated array. Looping thru
a copy of the mutated array fixes this problem.
2020-01-09 09:40:13 +01:00
Joseph Caburnay b7b0e38fca [FIX] docs: some spelling corrections 2020-01-09 09:40:13 +01:00
mcm-odoo 74bd119bf5 [IMP] styles: handle & selector
This commit adds the support of the & selector.
This selector is useful to join a subrule with a parent selector.

example:

button {
  &:hover {
    background-color: red;
  }
}

will give

button:hover {
  background-color: red;
}
2020-01-09 09:37:27 +01:00
Géry Debongnie 4a0d04e4ae [FIX] debug tools: do not crash on non json-stringifiable props
closes #595
2020-01-09 09:24:50 +01:00
Géry Debongnie 49931104a1 [CLEANUP] update snapshots 2020-01-08 16:05:22 +01:00
Lucas Perais (lpe) 21d1306ec2 [FIX] qweb: t-foreach needs to hold on scope
Have a t-on within a t-foreach
the t-on has an expression

Before this commit, when triggering the t-on, the expression was falsely evaluated
In details, the expression took scope from outside the foreach loop
as we protect it to have similar results than an actual for loop

But, at triggering time, the scope protection had already been terminated
meaning the info of the iteration of the loop the handler has been built in
had already disappeared

This commit fixes that

closes #594
2020-01-08 15:45:44 +01:00
Lucas Perais (lpe) 21737d33fa [FIX] qweb: variables set outside foreach must be altered by inloop t-tset
In a template, have t-set t-value outside a t-foreach
in the t-foreach, alter that variable by resetting it (as for a incrementation variable)

Before this commit, when printing the variable when the loop had finished
its value was the one set in the first place

After this commit, the value becomes the one altered by the loop iterations

closes #598
2020-01-08 15:45:44 +01:00
Géry Debongnie 23012f3e7c [FIX] component: multiple t-calls
Before this commit, owl could crash in some specific situations:
multiple t-calls with sub components, outside a loop.  The reason is
that the t-call did not generate a key, so from the point of view of the
children component, it had the same key, even though it was at a
different place in a template.

Also, with this fix, we can fix the playground responsive example.

closes #602
2020-01-08 15:24:43 +01:00
Géry Debongnie 55889febd5 [FIX] qweb: add support for "in " operator in expressions
closes #603
2020-01-08 15:20:01 +01:00
mcm-odoo 0af46bc123 [FIX] styles: fix selector generation
This commit fixes the css selector generation for nested rules

Before this fix, a selector like:

.parent {
	.child-a, .child-b {
		color: red;
	}
}

was generated as:

.parent .child-a, .child-b {
	color: red;
}

Now it generates:

.parent .child-a, .parent .child-b {
	color: red;
}
2019-12-27 12:31:17 +01:00
Géry Debongnie b63cd4c9d9 [REL] v1.0.0-beta5 2019-12-20 11:26:53 +01:00
Géry Debongnie 953778dc50 [IMP] component/tags: add inline css tag
This add an important feature: defining completely standalone owl
components, with the template/style and javascript code together.

closes #284
2019-12-20 11:20:10 +01:00
Géry Debongnie 4f61d9f1e0 [FIX] component: no crash if empty props
The expression parser properly format an empty string into an empty
string. But then, this empty string was injected in the props object of
a component, and the compiled code looked like this:

let props5 = { val: };

So, in this case, we simply put undefined, since there are no props
value.

closes #587
2019-12-19 14:32:10 +01:00
Sébastien Theys bc2c7edff4 [FIX] store: prevent crash when selector returns null
Because `typeof null === 'object'`.
2019-12-19 14:24:36 +01:00
Géry Debongnie c8d9c0b50e [DOC] add npm install instruction 2019-12-19 14:12:00 +01:00
Géry Debongnie 15b25fd838 [IMP] owl: prepare owl to be released on npm
closes #196
2019-12-19 14:09:41 +01:00
Géry Debongnie 1db0f5ac9b [REL] v1.0.0-beta4 2019-12-17 15:26:49 +01:00
Géry Debongnie 6465665550 [FIX] qweb/component: handle nested t-foreach and components
The way keys were handled in QWeb was mostly ok, but a little naive. It
is fine when we deal with a list of dom nodes, since the reconciliation
algorithm need only to be able to differentiate/reconcile nodes in that
list, but it is an issue with components, which needs to globally be
able to find themselves in their parent's children map.

Because of the way this was handled, there were situations were Owl
internal virtual dom would crash, since components wrongly assumed that
they were already rendered in a different place.

To fix this, we generalize the way keys are generated, by concatenating
all sub keys coming from each iteration loops.

closes #584
2019-12-17 15:23:24 +01:00
Géry Debongnie 9216c5c24b [FIX] qweb: t-call properly transfer key to sub components
Since t-call is now a function call, we need to properly handle the
internal key used to find previous components.  If a t-call is inside a
t-foreach, then we need to transfer the key to the sub template.
Otherwise, each component in the subtemplate will be associated to the
same key, which means that it will lead to big issues: components are
destroyed and reused...

closes #581
2019-12-16 20:30:34 +01:00
Géry Debongnie d9bf4284c6 [REL] v1.0.0-beta3 2019-12-16 09:56:35 +01:00
Géry Debongnie ef6fd457f8 [FIX] qweb: add support for spread operator
closes #575
2019-12-13 22:35:26 +01:00
Sébastien Theys 455e45c148 [FIX] store: make Array.map work correctly on the result of selector
closes #577
2019-12-13 20:32:02 +01:00
Lucas Perais (lpe) 5e7f1d5e6d [FIX] qweb: t-call must pass extra parent
Before this commit, when template was t-call'ed and defined
t-component directive within it, there was a crash because extra.parent was never defined

After this commit, this use case works
2019-12-13 20:28:49 +01:00
Géry Debongnie 0b05ad619f [FIX] qweb: t-set with a body should not be joined with comma
closes #574
2019-12-13 20:15:37 +01:00
Géry Debongnie 29a80d0b28 [FIX] qweb: properly display falsy values
While it is not tested, nor documented, the reference QWeb
implementation display the `false` value as "false". So, we have to
adapt the Owl implementation to match that behaviour.

At the same time, this commit uses 'let' instead of 'var' in various places,
to make the compiled code more consistant.
2019-12-13 14:48:19 +01:00
Géry Debongnie 0931a4dc5b [FIX] qweb: bind handlers to component
Before this commit, the handlers were bound to the current context,
which is often the component but not necessarily.  In some cases, a sub
scope can be created (with t-foreach, or slots, or t-call), and the
context is actually an object with the actual component instance it its
prototype chain, but not the component.
2019-12-12 14:32:43 +01:00
Géry Debongnie 4e22dbcad6 [FIX] qweb: accept assignations in qweb expressions
closes #560
closes #569
2019-12-12 14:32:43 +01:00
Géry Debongnie b283e65ad4 [REF] qweb: improve t-on generated code
and deduplicate logic between directive t-on applied on a node and on a
component
2019-12-12 14:32:43 +01:00
Géry Debongnie f517d44e32 [IMP] qweb: slightly simplify generated templates
- In some cases, we can simply inline a template key instead of assigning
  it to a variable and then using that variable
- Add component name in generated code for components
2019-12-12 09:39:26 +01:00
Lucas Perais (lpe) 08022b49df [FIX] qweb: conditional t-call with body
When a t-call has a condition and a body
the t-if's and its flavors should be ignored when compiling the body of a t-call
2019-12-11 14:09:45 +01:00
Géry Debongnie b96ea79f2b [FIX] qweb: renderToString now properly escape
closes #565
2019-12-11 12:25:05 +01:00
Géry Debongnie 97d8b3ed8c [REF] tests: remove Widget common class
Bad name, and bad practice to use it. It makes tests less standalone.
2019-12-11 12:25:05 +01:00
Géry Debongnie 9921ae07b1 [REF] tests: move slots tests out of main component file 2019-12-11 12:25:05 +01:00
Géry Debongnie 41cb5f1abd [FIX] *: remove useless values to t-else and t-debug
The actual value does not matter, only the fact that it is present.

closes #564
2019-12-11 12:25:05 +01:00
57 changed files with 13972 additions and 5734 deletions
+3 -1
View File
@@ -23,4 +23,6 @@ package-lock.json
node_modules
# Extras temp file
/tools/owl.js
/tools/owl.js
release-notes.md
+8 -2
View File
@@ -102,10 +102,16 @@ Submit a PR!
## Installing/Building
Owl can be installed with the following command:
```
npm install @odoo/owl
```
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.0.0-beta2.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta2/owl.js)
- [owl-1.0.0-beta2.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta2/owl.min.js)
- [owl-1.0.4.js](https://github.com/odoo/owl/releases/download/v1.0.4/owl.js)
- [owl-1.0.4.min.js](https://github.com/odoo/owl/releases/download/v1.0.4/owl.min.js)
Some npm scripts are available:
+9 -9
View File
@@ -56,15 +56,15 @@ useState Link
config RouteComponent
mode Router
core tags
EventBus xml
Observer utils
hooks debounce
onWillStart escape
onMounted loadJS
onWillUpdateProps loadFile
onWillPatch shallowEqual
onPatched whenReady
onWillUnmount
EventBus css
Observer xml
hooks utils
onWillStart debounce
onMounted escape
onWillUpdateProps loadJS
onWillPatch loadFile
onPatched shallowEqual
onWillUnmount whenReady
useContext
useState
useRef
+17 -3
View File
@@ -225,6 +225,10 @@ to be called in the constructor.
}
```
- **`style`** (string, optional): it should be the return value of the [`css` tag](tags.md#css-tag),
which is used to inject stylesheet whenever the component is visible on the
screen.
There is another static property defined on the `Component` class: `current`.
This property is set to the currently being defined component (in the constructor).
This is the way [hooks](hooks.md) are able to get a reference to the target
@@ -421,7 +425,7 @@ likely via a change in its state/props or environment).
This method is not called on the initial render. It is useful to interact
with the DOM (for example, through an external library) whenever the
component was patched. Note that this hook will not be called if the compoent is
component was patched. Note that this hook will not be called if the component is
not in the DOM.
Updating the component state in this hook is possible, but not encouraged.
@@ -628,10 +632,20 @@ as it will be translated to:
```js
button.addEventListener("click", () => {
component.state.counter++;
context.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
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
specified as additional suffixes of the `t-on` directive.
@@ -946,7 +960,7 @@ For example, here is how we could implement an `ErrorBoundary` component:
<t t-if="state.error">
Error handled
</t>
<t t-else="1">
<t t-else="">
<t t-slot="default" />
</t>
</div>
+2 -2
View File
@@ -23,7 +23,7 @@ a rendering that is no longer relevant, restart it, reuse it in some cases.
But even though using concurrency is quite simple (and is the default behaviour),
asynchrony is difficult, because it introduces an additional dimension that
vastly increase the complexity of an application. This section will explain
how Owl manages this complexity, how concuurent rendering works in a general way.
how Owl manages this complexity, how concurrent rendering works in a general way.
## Rendering Components
@@ -120,7 +120,7 @@ Here is what Owl will do:
1. hook `willUpdateProps` is called on `D` (async)
2. template `D` is rerendered
- component `F` is created:
1. hook `willStart` is called on `E` (async)
1. hook `willStart` is called on `F` (async)
2. template `F` is rendered
3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`,
+1 -1
View File
@@ -57,7 +57,7 @@ class SomeComponent extends Component {
<t t-if=device.isMobile>
some simplified user interface
</t>
<t t-else="1">
<t t-else="">
a more advanced user interface
</t>
</div>`;
+2 -2
View File
@@ -40,8 +40,8 @@ all templates.
Whenever a root component `App` is mounted, Owl will setup a valid environment by
following the next steps:
- take the `env` object defined on `App.env` (if no `env` was explicitely setup,
this will be return the empty `env` object defined on `Component`)
- take the `env` object defined on `App.env` (if no `env` was explicitly setup,
this will return the empty `env` object defined on `Component`)
- if `env.qweb` is not set, then Owl will create a `QWeb` instance.
The correct way to customize an environment is to simply set it up on the root
+12
View File
@@ -17,6 +17,7 @@
- [`useContext`](#usecontext)
- [`useRef`](#useref)
- [`useSubEnv`](#usesubenv)
- [`useExternalListener`](#useexternallistener)
- [`useStore`](#usestore)
- [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters)
@@ -354,6 +355,17 @@ will be added to the parent environment. Note that it will extend, not replace
the parent environment. And of course, the parent environment will not be
affected.
### `useExternalListener`
The `useExternalListener` hook helps solve a very common problem: adding and removing
a listener on some target whenever a component is mounted/unmounted. For example,
a dropdown menu (or its parent) may need to listen to a `click` event on `window`
to be closed:
```js
useExternalListener(window, "click", this.closeMenu);
```
### `useStore`
The `useStore` hook is the entry point for a component to connect to the store.
+2 -2
View File
@@ -28,7 +28,7 @@ generate a virtual dom representation of the HTML.
```xml
<div>
<span t-if="somecondition">Some string</span>
<ul t-else="1">
<ul t-else="">
<li t-foreach="messages" t-as="message">
<t t-esc="message"/>
</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 ––>
<t>
<div t-if="someCondition">foo</div>
<span t-else="1">bar</span>
<span t-else="">bar</span>
</t>
```
+13 -1
View File
@@ -208,7 +208,11 @@ Note that getters are not cached.
### Connecting a Component
At some point, we need a way to interact with the store from a component. This
can be done with the help of the three store hooks:
means that the component needs a reference to the store. By default, it looks
for it in the `env.store` key. However, this can be configured with the `useStore`
hook.
Every component-store interactions are done with the help of the three store hooks:
- [`useStore`](#usestore) to subscribe a component to some part of the store state,
- [`useDispatch`](#usedispatch) to get a reference to a dispatch function,
@@ -229,6 +233,14 @@ const state = {
const store = new owl.Store({ state, actions });
```
To make it accessible to the complete application, we will put it in the
environment:
```js
// in this example, the root component is App
App.env.store = store;
```
A counter component can then select this value and dispatch an action like this:
```js
+134 -4
View File
@@ -4,16 +4,19 @@
- [Overview](#overview)
- [`xml` tag](#xml-tag)
- [`css` tag](#css-tag)
## Overview
Tags are very small helpers to make it easy to write inline templates. There is
only one currently available tag: `xml`, but we plan to add other tags later,
such as a `css` tag, which will be used to write [single file components](../tooling.md#single-file-component).
Tags are very small helpers intended to make it easy to write inline templates
or styles. There are currently two tags: `css` and `xml`. With these functions,
it is possible to write [single file components](../tooling.md#single-file-component).
## XML tag
Without tags, creating a standalone component would look like this:
The `xml` tag is certainly the most useful tag. It is used to define an inline
QWeb template for a component. Without tags, creating a standalone component
would look like this:
```js
import { Component } from 'owl'
@@ -52,3 +55,130 @@ class MyComponent extends Component {
...
}
```
## CSS tag
The CSS tag is useful to define a css stylesheet in the javascript file:
```js
class MyComponent extends Component {
static template = xml`
<div class="my-component">some template</div>
`;
static css`
.my-component {
color: red;
}
`;
}
```
The `css` tag registers internally the css information. Then, whenever the first
instance of the component is created, will add a `<style>` tag to the document
`<head>`.
Note that to make it more useful, like other css preprocessors, the `css` tag
accepts a small extension of the css specification: css scopes can be nested,
and the rules will then be expanded by the `css` helper:
```scss
.my-component {
display: block;
.sub-component h {
color: red;
}
}
```
will be formatted as:
```css
.my-component {
display: block;
}
.my-component .sub-component h {
color: red;
}
```
This extension brings another useful feature: the `&` selector which refers to
the parent selector. For example, we want our component to be red when hovered.
We would like to write something like:
```scss
.my-component {
display: block;
:hover {
color: red;
}
}
```
but it will be formatted as:
```css
.my-component {
display: block;
}
.my-component :hover {
color: red;
}
```
The `&` selector can be used to solve this problem:
```scss
.my-component {
display: block;
&:hover {
color: red;
}
}
```
will be formatted as:
```css
.my-component {
display: block;
}
.my-component:hover {
color: red;
}
```
Now, there is no additional processing done by the `css` tag. However, since it
is done in javascript at runtime, we actually have more power. For example:
1. sharing values between javascript and css:
```js
import { theme } from "./theme";
class MyComponent extends Component {
static template = xml`<div class="my-component">...</div>`;
static style = css`
.my-component {
color: ${theme.MAIN_COLOR};
background-color: ${theme.SECONDARY_color};
}
`;
}
```
2. scoping rules to the current component:
```js
import { generateUUID } from "./utils";
const uuid = generateUUID();
class MyComponent extends Component {
static template = xml`<div data-o-${uuid}="">...</div>`;
static style = css`
[data-o-${uuid}] {
color: red;
}
`;
}
```
+21 -10
View File
@@ -43,32 +43,43 @@ useful to compare various performance metrics on some tasks.
It is very useful to group code by feature instead of by type of file. It makes
it easier to scale application to larger size.
To do so, Owl currently has a small helper that makes it easy to define a
template inside a javascript (or typescript) file: the [`xml`](reference/tags.md#xml-tag)
helper. With this, a template is automatically registered to [QWeb](reference/qweb_engine.md).
To do so, Owl has two small helpers that make it easy to define a
template or a stylesheet inside a javascript (or typescript) file: the
[`xml`](reference/tags.md#xml-tag) and [`css`](reference/tags.md#css-tag)
helper.
This means that the template and the javascript code can be defined in the same
file. It is not currently possible to add css to the same file, but Owl may
get a `css` tag helper later.
This means that the template, the style and the javascript code can be defined in
the same file. For example:
```js
const { Component } = owl;
const { xml } = owl.tags;
const { xml, css } = owl.tags;
// -----------------------------------------------------------------------------
// TEMPLATE
// -----------------------------------------------------------------------------
const TEMPLATE = xml/* xml */ `
<div class="main two-columns">
<div class="main">
<Sidebar/>
<Content />
</div>`;
// -----------------------------------------------------------------------------
// STYLE
// -----------------------------------------------------------------------------
const STYLE = css/* css */ `
.main {
display: grid;
grid-template-columns: 200px auto;
}
`;
// -----------------------------------------------------------------------------
// CODE
// -----------------------------------------------------------------------------
class MyComponent extends Component {
class Main extends Component {
static template = TEMPLATE;
static style = STYLE;
static components = { Sidebar, Content };
// rest of component...
@@ -91,7 +102,7 @@ logging useful information is extremely valuable. There is a [javascript file](.
Once it is executed, it will log a lot of information on each component main hooks. The following code is a minified version to make it easier to copy/paste:
```
function debugOwl(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)}}}
function debugOwl(t,e){let n,o="[OWL_DEBUG]";function r(t){let e;try{e=JSON.stringify(t||{})}catch(t){e="<JSON error>"}return e.length>200&&(e=e.slice(0,200)+"..."),e}if(Object.defineProperty(t.Component,"current",{get:()=>n,set(s){n=s;const i=s.constructor.name;if(e.componentBlackList&&e.componentBlackList.test(i))return;if(e.componentWhiteList&&!e.componentWhiteList.test(i))return;let l;Object.defineProperty(n,"__owl__",{get:()=>l,set(n){!function(n,s,i){let l=`${s}<id=${i}>`,c=t=>console.log(`${o} ${l} ${t}`),u=t=>(!e.methodBlackList||!e.methodBlackList.includes(t))&&!(e.methodWhiteList&&!e.methodWhiteList.includes(t));u("constructor")&&c(`constructor, props=${r(n.props)}`);u("willStart")&&t.hooks.onWillStart(()=>{c("willStart")});u("mounted")&&t.hooks.onMounted(()=>{c("mounted")});u("willUpdateProps")&&t.hooks.onWillUpdateProps(t=>{c(`willUpdateProps, nextprops=${r(t)}`)});u("willPatch")&&t.hooks.onWillPatch(()=>{c("willPatch")});u("patched")&&t.hooks.onPatched(()=>{c("patched")});u("willUnmount")&&t.hooks.onWillUnmount(()=>{c("willUnmount")});const d=n.__render.bind(n);n.__render=function(...t){c("rendering template"),d(...t)};const h=n.render.bind(n);n.render=function(...t){const e=n.__owl__;let o="render";return e.isMounted||e.currentFiber||(o+=" (warning: component is not mounted, this render has no effect)"),c(o),h(...t)};const p=n.mount.bind(n);n.mount=function(...t){return c("mount"),p(...t)}}(s,i,(l=n).id)}})}}),e.logScheduler){let e=t.Component.scheduler.start,n=t.Component.scheduler.stop;t.Component.scheduler.start=function(){this.isRunning||console.log(`${o} scheduler: start running tasks queue`),e.call(this)},t.Component.scheduler.stop=function(){this.isRunning&&console.log(`${o} scheduler: stop running tasks queue`),n.call(this)}}if(e.logStore){let e=t.Store.prototype.dispatch;t.Store.prototype.dispatch=function(t,...n){return console.log(`${o} store: action '${t}' dispatched. Payload: '${r(n)}'`),e.call(this,t,...n)}}}
debugOwl(owl, {
// componentBlackList: /App/, // regexp
// componentWhiteList: /SomeComponent/, // regexp
+13 -4
View File
@@ -1,8 +1,13 @@
{
"name": "owl-framework",
"version": "1.0.0-beta2",
"name": "@odoo/owl",
"version": "1.0.4",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"main": "dist/owl.js",
"types": "dist/types/index.d.ts",
"files": [
"dist/types/",
"dist/owl.js"
],
"engines": {
"node": ">=10.15.3"
},
@@ -18,7 +23,9 @@
"tools": "npm run build && npm run tools:serve",
"pretools:watch": "npm run build",
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write"
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
"publish": "npm run buildcommonjs && npm publish",
"release": "node tools/release.js"
},
"repository": {
"type": "git",
@@ -33,8 +40,10 @@
"dependencies": {},
"devDependencies": {
"@types/jest": "^23.3.12",
"chalk": "^3.0.0",
"cpx": "^1.5.0",
"git-rev-sync": "^1.12.0",
"github-api": "^3.3.0",
"jest": "^23.6.0",
"jest-environment-jsdom": "^24.7.1",
"live-server": "^1.2.1",
+2 -2
View File
@@ -1,7 +1,7 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.0.0-beta2
- Status: mostly stable
- Current version: 1.0.4
- Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may
change!
+26 -1
View File
@@ -6,6 +6,7 @@ import "./directive";
import { Fiber } from "./fiber";
import "./props_validation";
import { Scheduler, scheduler } from "./scheduler";
import { activateSheet } from "./styles";
/**
* Owl Component System
@@ -199,6 +200,9 @@ export class Component<T extends Env, Props extends {}> {
refs: null,
scope: null
};
if (constr.style) {
this.__applyStyles(constr);
}
}
/**
@@ -298,7 +302,14 @@ export class Component<T extends Env, Props extends {}> {
const position = options.position || "last-child";
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
return Promise.resolve();
if (position !== "self" && this.el!.parentNode !== target) {
// in this situation, we are trying to mount a component on a different
// target. In this case, we need to unmount first, otherwise it will
// not work.
this.unmount();
} else {
return Promise.resolve();
}
}
if (!(target instanceof HTMLElement || target instanceof DocumentFragment)) {
let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`;
@@ -580,6 +591,20 @@ export class Component<T extends Env, Props extends {}> {
return fiber;
}
/**
* Apply the stylesheets defined by the component. Note that we need to make
* sure all inherited stylesheets are applied as well. We then delete the
* `style` key from the constructor to make sure we do not apply it again.
*/
private __applyStyles(constr) {
while (constr && constr.style) {
if (constr.hasOwnProperty("style")) {
activateSheet(constr.style, constr.name);
delete constr.style;
}
constr = constr.__proto__;
}
}
__getTemplate(qweb: QWeb): string {
let p = (<any>this).constructor;
if (!p.hasOwnProperty("_template")) {
+14 -37
View File
@@ -1,6 +1,6 @@
import { QWeb } from "../qweb/index";
import { INTERP_REGEXP } from "../qweb/compilation_context";
import { MODS_CODE } from "../qweb/extensions";
import { makeHandlerCode, MODS_CODE } from "../qweb/extensions";
//------------------------------------------------------------------------------
// t-component
@@ -189,7 +189,7 @@ QWeb.addDirective({
extraNames: ["props"],
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//COMPONENT");
ctx.addLine(`// Component '${value}'`);
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true;
@@ -197,7 +197,7 @@ QWeb.addDirective({
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
// t-on- events and t-transition
const events: [string, string[], string, string][] = [];
const events: [string, string][] = [];
let transition: string = "";
const attributes = (<Element>node).attributes;
const props: { [key: string]: string } = {};
@@ -205,19 +205,13 @@ QWeb.addDirective({
const name = attributes[i].name;
const value = attributes[i].textContent!;
if (name.startsWith("t-on-")) {
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]);
events.push([name, value]);
} else if (name === "t-transition") {
transition = value;
} else if (!name.startsWith("t-")) {
if (name !== "class" && name !== "style") {
// this is a prop!
props[name] = ctx.formatExpression(value);
props[name] = ctx.formatExpression(value) || "undefined";
}
}
}
@@ -284,32 +278,15 @@ QWeb.addDirective({
}
}
let eventsCode = events
.map(function([eventName, mods, handlerValue, extraArgs]) {
let params = "context";
if (extraArgs) {
if (ctx.loopNumber) {
let argId = ctx.generateID();
// we need to evaluate the arguments now, because the handler will
// be set asynchronously later when the widget is ready, and the
// context might be different.
ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`);
params = `context, arg${argId}`;
} else {
params = `context, ${ctx.formatExpression(extraArgs)}`;
}
}
let handler = `function (e) {if(!context.__owl__.isMounted){return}`;
handler += mods
.map(function(mod) {
return T_COMPONENT_MODS_CODE[mod];
})
.join("");
if (handlerValue) {
handler += `const fn = context['${handlerValue}'];`;
handler += `if (fn) { fn.call(${params}, e); } else { context.${handlerValue}; }`;
}
handler += `}`;
return `vn.elm.addEventListener('${eventName}', ${handler});`;
.map(function([name, value]) {
const { event, handler } = makeHandlerCode(
ctx,
name,
value,
false,
T_COMPONENT_MODS_CODE
);
return `vn.elm.addEventListener('${event}', ${handler});`;
})
.join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
+70
View File
@@ -0,0 +1,70 @@
/**
* Owl Style System
*
* This files contains the Owl code related to processing (extended) css strings
* and creating/adding <style> tags to the document head.
*/
export const STYLESHEETS: { [id: string]: HTMLStyleElement } = {};
export function processSheet(str: string): string {
const tokens = str.split(/(\{|\}|;)/).map(s => s.trim());
const selectorStack: string[][] = [];
const parts: string[] = [];
let rules: string[] = [];
function generateSelector(stackIndex: number, parentSelector?: string) {
const parts: string[] = [];
for (const selector of selectorStack[stackIndex]) {
let part = (parentSelector && parentSelector + " " + selector) || selector;
if (part.includes("&")) {
part = selector.replace(/&/g, parentSelector || "");
}
if (stackIndex < selectorStack.length - 1) {
part = generateSelector(stackIndex + 1, part);
}
parts.push(part);
}
return parts.join(", ");
}
function generateRules() {
if (rules.length) {
parts.push(generateSelector(0) + " {");
parts.push(...rules);
parts.push("}");
rules = [];
}
}
while (tokens.length) {
let token = tokens.shift()!;
if (token === "}") {
generateRules();
selectorStack.pop();
} else {
if (tokens[0] === "{") {
generateRules();
selectorStack.push(token.split(/\s*,\s*/));
tokens.shift();
}
if (tokens[0] === ";") {
rules.push(" " + token + ";");
}
}
}
return parts.join("\n");
}
export function registerSheet(id: string, css: string) {
const sheet = document.createElement("style");
sheet.innerHTML = processSheet(css);
STYLESHEETS[id] = sheet;
}
export function activateSheet(id, name) {
const sheet = STYLESHEETS[id];
if (!sheet) {
throw new Error(
`Invalid css stylesheet for component '${name}'. Did you forget to use the 'css' tag helper?`
);
}
sheet.setAttribute("component", name);
document.head.appendChild(sheet);
}
+29
View File
@@ -131,3 +131,32 @@ export function useSubEnv(nextEnv) {
const component = Component.current!;
component.env = Object.assign(Object.create(component.env), nextEnv);
}
// -----------------------------------------------------------------------------
// useExternalListener
// -----------------------------------------------------------------------------
/**
* When a component needs to listen to DOM Events on element(s) that are not
* part of his hierarchy, we can use the `useExternalListener` hook.
* It will correctly add and remove the event listener, whenever the
* component is mounted and unmounted.
*
* Example:
* a menu needs to listen to the click on window to be closed automatically
*
* Usage:
* in the constructor of the OWL component that needs to be notified,
* `useExternalListener(window, 'click', this._doSomething);`
* */
export function useExternalListener(
target: HTMLElement | typeof window,
eventName: string,
handler,
eventParams?
) {
const boundHandler = handler.bind(Component.current);
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
}
+34 -18
View File
@@ -1,6 +1,7 @@
import { CompilationContext } from "./compilation_context";
import { QWeb } from "./qweb";
import { htmlToVDOM } from "../vdom/html_to_vdom";
import { QWebVar } from "./expression_parser";
/**
* Owl QWeb Directives
@@ -39,11 +40,11 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
let exprID: string;
if (typeof value === "string") {
exprID = `_${ctx.generateID()}`;
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
ctx.addLine(`let ${exprID} = ${ctx.formatExpression(value)};`);
} else {
exprID = `scope.${value.id}`;
}
ctx.addIf(`${exprID} || ${exprID} === 0`);
ctx.addIf(`${exprID} != null`);
if (ctx.escaping) {
let protectID;
@@ -61,7 +62,7 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
let nodeID = ctx.generateID();
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`);
if (ctx.rootContext.shouldDefineResult) {
ctx.addLine(`result = vn${nodeID}`);
}
@@ -119,7 +120,7 @@ QWeb.addDirective({
ctx.rootContext.shouldDefineScope = true;
const variable = node.getAttribute("t-set")!;
let value = node.getAttribute("t-value")!;
ctx.variables[variable] = ctx.variables[variable] || {};
ctx.variables[variable] = ctx.variables[variable] || ({} as QWebVar);
let qwebvar = ctx.variables[variable];
const hasBody = node.hasChildNodes();
@@ -127,7 +128,12 @@ QWeb.addDirective({
qwebvar.expr = `scope.${variable}`;
if (value) {
const formattedValue = ctx.formatExpression(value);
ctx.addLine(`${qwebvar.expr} = ${formattedValue};`);
let scopeExpr = `scope`;
if (ctx.protectedScopeNumber) {
ctx.rootContext.shouldDefineUtils = true;
scopeExpr = `utils.getScope(scope, '${variable}')`;
}
ctx.addLine(`${scopeExpr}.${variable} = ${formattedValue};`);
qwebvar.value = formattedValue;
}
@@ -140,7 +146,7 @@ QWeb.addDirective({
const _parentNode = ctx.parentNode;
ctx.parentNode = tempParentNodeID;
ctx.addLine(`const c${tempParentNodeID} = new utils.VDomArray();`);
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);
@@ -215,6 +221,7 @@ QWeb.addDirective({
// Step 1: sanity checks
// ------------------------------------------------
ctx.rootContext.shouldDefineScope = true;
ctx.rootContext.shouldDefineUtils = true;
if (node.nodeName !== "t") {
throw new Error("Invalid tag for t-call directive (should be 't')");
}
@@ -228,7 +235,7 @@ QWeb.addDirective({
// ------------------------------------------------
if (!qweb.subTemplates[subTemplate]) {
qweb.subTemplates[subTemplate] = true;
const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx);
const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx, true);
qweb.subTemplates[subTemplate] = subTemplateFn;
}
@@ -242,7 +249,9 @@ QWeb.addDirective({
ctx.indent();
protectID = ctx.startProtectScope();
const nodeCopy = node.cloneNode(true) as Element;
nodeCopy.removeAttribute("t-call");
for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) {
nodeCopy.removeAttribute(attr);
}
const parentNode = ctx.parentNode;
ctx.parentNode = "__0";
// this local scope is intended to trap c__0
@@ -260,17 +269,17 @@ QWeb.addDirective({
// Step 4: add the appropriate function call to current component
// ------------------------------------------------
const callingScope = hasBody ? "scope" : "Object.assign(Object.create(context), scope)";
const parentComponent = `utils.getComponent(context)`;
const key = ctx.generateTemplateKey();
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`;
if (ctx.parentNode) {
ctx.addLine(
`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, Object.assign({}, extra, {parentNode: c${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}, Object.assign({}, extra, {parentNode: result}));`
);
ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, ${extra});`);
ctx.addLine(`result = result[0]`);
}
@@ -299,17 +308,17 @@ QWeb.addDirective({
const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID();
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine(`let _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
let keysID = ctx.generateID();
let valuesID = ctx.generateID();
ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
ctx.addLine(`let _${keysID} = _${valuesID} = _${arrayID};`);
ctx.addIf(`!(_${arrayID} instanceof Array)`);
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf();
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
let varsID = ctx.startProtectScope();
ctx.addLine(`let _length${keysID} = _${keysID}.length;`);
let varsID = ctx.startProtectScope(true);
const loopVar = `i${ctx.loopNumber}`;
ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
ctx.indent();
@@ -330,6 +339,13 @@ QWeb.addDirective({
`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");
qweb._compileNode(nodeCopy, ctx);
+41 -18
View File
@@ -1,4 +1,4 @@
import { compileExpr, QWebVar } from "./expression_parser";
import { compileExpr, compileExprToArray, QWebVar } from "./expression_parser";
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------
@@ -17,6 +17,7 @@ export class CompilationContext {
rootContext: CompilationContext;
shouldDefineParent: boolean = false;
shouldDefineScope: boolean = false;
protectedScopeNumber: number = 0;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldDefineRefs: boolean = false;
@@ -26,12 +27,13 @@ export class CompilationContext {
templateName: string;
allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false;
currentKey: string = "";
hasKey0: boolean = false;
keyStack: boolean[] = [];
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
this.addLine("var h = this.h;");
this.addLine("let h = this.h;");
}
generateID(): number {
@@ -46,19 +48,17 @@ export class CompilationContext {
* Such a key is necessary when we need to associate an id to some element
* generated by a template (for example, a component)
*/
generateTemplateKey(): string {
generateTemplateKey(prefix: string = ""): string {
const id = this.generateID();
let locationExpr = `\`__${this.generateID()}__`;
for (let i = 0; i < this.loopNumber - 1; i++) {
locationExpr += `\${i${i + 1}}__`;
if (this.loopNumber === 0 && !this.hasKey0) {
return `'${prefix}__${id}__'`;
}
if (this.currentKey) {
const k = this.currentKey;
this.addLine(`let k${id} = ${locationExpr}\` + ${k};`);
} else {
locationExpr += this.loopNumber ? `\${i${this.loopNumber}}__\`` : "`";
this.addLine(`let k${id} = ${locationExpr};`);
let key = `\`${prefix}__${id}__`;
let start = this.hasKey0 ? 0 : 1;
for (let i = start; i < this.loopNumber + 1; i++) {
key += `\${key${i}}__`;
}
this.addLine(`let k${id} = ${key}\`;`);
return `k${id}`;
}
@@ -113,11 +113,11 @@ export class CompilationContext {
}
indent() {
this.indentLevel++;
this.rootContext.indentLevel++;
}
dedent() {
this.indentLevel--;
this.rootContext.indentLevel--;
}
addLine(line: string): number {
@@ -156,6 +156,24 @@ export class CompilationContext {
this.rootContext.shouldDefineScope = true;
return compileExpr(expr, this.variables);
}
captureExpression(expr: string): string {
this.rootContext.shouldDefineScope = true;
const argId = this.generateID();
const tokens = compileExprToArray(expr, this.variables);
const done = new Set();
return tokens
.map(tok => {
if (tok.varName) {
if (!done.has(tok.varName)) {
done.add(tok.varName);
this.addLine(`const ${tok.varName}_${argId} = ${tok.value};`);
}
tok.value = `${tok.varName}_${argId}`;
}
return tok.value;
})
.join("");
}
/**
* Perform string interpolation on the given string. Note that if the whole
@@ -174,14 +192,19 @@ export class CompilationContext {
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`";
}
startProtectScope(): number {
startProtectScope(codeBlock?: boolean): number {
const protectID = this.generateID();
this.rootContext.protectedScopeNumber++;
this.rootContext.shouldDefineScope = true;
this.addLine(`const _origScope${protectID} = scope;`);
this.addLine(`scope = Object.assign(Object.create(context), scope);`);
const scopeExpr = codeBlock
? `Object.create(scope);`
: `Object.assign(Object.create(context), scope);`;
this.addLine(`let _origScope${protectID} = scope;`);
this.addLine(`scope = ${scopeExpr}`);
return protectID;
}
stopProtectScope(protectID: number) {
this.rootContext.protectedScopeNumber--;
this.addLine(`scope = _origScope${protectID};`);
}
}
+11 -3
View File
@@ -66,6 +66,7 @@ interface Token {
value: string;
originalValue?: string;
size?: number;
varName?: string;
}
const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
@@ -81,7 +82,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
// note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>".split(",");
const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ".split(",");
type Tokenizer = (expr: string) => Token | false;
@@ -234,7 +235,7 @@ export function tokenize(expr: string): Token[] {
* 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 compileExprToArray(expr: string, scope: { [key: string]: QWebVar }): Token[] {
scope = Object.create(scope);
const tokens = tokenize(expr);
for (let i = 0; i < tokens.length; i++) {
@@ -269,6 +270,7 @@ export function compileExpr(expr: string, scope: { [key: string]: QWebVar }): st
}
if (isVar) {
token.varName = token.value;
if (token.value in scope && "id" in scope[token.value]) {
token.value = scope[token.value].expr!;
} else {
@@ -277,5 +279,11 @@ export function compileExpr(expr: string, scope: { [key: string]: QWebVar }): st
}
}
}
return tokens.map(t => t.value).join("");
return tokens;
}
export function compileExpr(expr: string, scope: { [key: string]: QWebVar }): string {
return compileExprToArray(expr, scope)
.map(t => t.value)
.join("");
}
+72 -42
View File
@@ -26,48 +26,67 @@ export const MODS_CODE = {
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) {
const 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
// we need to capture every variable in it
putInCache = false;
code = ctx.captureExpression(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({
name: "on",
priority: 90,
atNodeCreation({ ctx, fullName, value, nodeID }) {
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 ? `context, ${ctx.formatExpression(extraArgs)}` : "context";
let handler = `function (e) {if (!context.__owl__.isMounted){return}`;
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};`);
}
const { event, handler } = makeHandlerCode(ctx, fullName, value, true);
ctx.addLine(`p${nodeID}.on['${event}'] = ${handler};`);
}
});
@@ -285,9 +304,20 @@ QWeb.addDirective({
QWeb.addDirective({
name: "key",
priority: 45,
atNodeEncounter({ ctx, value }) {
let id = ctx.generateID();
ctx.addLine(`const nodeKey${id} = ${ctx.formatExpression(value)};`);
ctx.currentKey = `nodeKey${id}`;
atNodeEncounter({ ctx, value, node }) {
if (ctx.loopNumber === 0) {
ctx.keyStack.push(ctx.rootContext.hasKey0);
ctx.rootContext.hasKey0 = true;
}
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;
}
}
});
+57 -18
View File
@@ -1,7 +1,7 @@
import { EventBus } from "../core/event_bus";
import { h, patch, VNode } from "../vdom/index";
import { CompilationContext } from "./compilation_context";
import { shallowEqual } from "../utils";
import { shallowEqual, escape } from "../utils";
import { addNS } from "../vdom/vdom";
/**
@@ -86,6 +86,10 @@ interface Utils {
[key: string]: any;
}
function isComponent(obj) {
return obj && obj.hasOwnProperty("__owl__");
}
const UTILS: Utils = {
zero: Symbol("zero"),
toObj(expr) {
@@ -119,7 +123,24 @@ const UTILS: Utils = {
return vnode.text;
}
})
.join();
.join("");
},
getComponent(obj) {
while (obj && !isComponent(obj)) {
obj = obj.__proto__;
}
return obj;
},
getScope(obj, property: string) {
const obj0 = obj;
while (obj && !obj.hasOwnProperty(property)) {
const newObj = obj.__proto__;
if (!newObj || isComponent(newObj)) {
return obj0;
}
obj = newObj;
}
return obj;
}
};
@@ -348,8 +369,17 @@ export class QWeb extends EventBus {
return vnode.text!;
}
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
const elem = patch(node, vnode).elm as HTMLElement;
function escapeTextNodes(node) {
if (node.nodeType === 3) {
node.textContent = escape(node.textContent);
}
for (let n of node.childNodes) {
escapeTextNodes(n);
}
}
escapeTextNodes(elem);
return elem.outerHTML;
}
/**
@@ -368,7 +398,12 @@ export class QWeb extends EventBus {
});
}
_compile(name: string, elem: Element, parentContext?: CompilationContext): CompiledTemplate {
_compile(
name: string,
elem: Element,
parentContext?: CompilationContext,
defineKey?: boolean
): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new CompilationContext(name);
if (elem.tagName !== "t") {
@@ -381,6 +416,10 @@ export class QWeb extends EventBus {
ctx.hasParentWidget = true;
ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
if (defineKey) {
ctx.addLine(`let key0 = extra.key || "";`);
ctx.hasKey0 = true;
}
}
this._compileNode(elem, ctx);
@@ -451,16 +490,13 @@ export class QWeb extends EventBus {
// this is an unusual situation: this text node is the result of the
// template rendering.
let nodeID = ctx.generateID();
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
ctx.addLine(`let vn${nodeID} = {text: \`${text}\`};`);
ctx.addLine(`result = vn${nodeID};`);
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
}
return;
}
if (ctx !== ctx.rootContext) {
ctx = ctx.subContext("currentKey", ctx.currentKey);
}
const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) {
@@ -663,7 +699,7 @@ export class QWeb extends EventBus {
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
} else {
ctx.addLine(`var _${attID} = '${value}';`);
ctx.addLine(`let _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
@@ -699,12 +735,12 @@ export class QWeb extends EventBus {
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
ctx.addLine(`let _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1);
}
ctx.addLine(`var _${attID} = ${formattedValue};`);
ctx.addLine(`let _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
}
@@ -720,9 +756,9 @@ export class QWeb extends EventBus {
const attID = ctx.generateID();
let staticVal = (<Element>node).getAttribute(attName);
if (staticVal) {
ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
ctx.addLine(`let _${attID} = '${staticVal} ' + ${formattedExpr};`);
} else {
ctx.addLine(`var _${attID} = ${formattedExpr};`);
ctx.addLine(`let _${attID} = ${formattedExpr};`);
}
attrs.push(`${attName}: _${attID}`);
}
@@ -730,13 +766,13 @@ export class QWeb extends EventBus {
// t-att= attributes
if (name === "t-att") {
let id = ctx.generateID();
ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
ctx.addLine(`let _${id} = ${ctx.formatExpression(value!)};`);
tattrs.push(id);
}
}
let nodeID = ctx.generateID();
let nodeKey = ctx.currentKey || nodeID;
const parts = [`key:${nodeKey}`];
let key = ctx.loopNumber || ctx.hasKey0 ? `\`\${key${ctx.loopNumber}}_${nodeID}\`` : nodeID;
const parts = [`key:${key}`];
if (attrs.length + tattrs.length > 0) {
parts.push(`attrs:{${attrs.join(",")}}`);
}
@@ -762,9 +798,12 @@ export class QWeb extends EventBus {
ctx.addLine(`}`);
ctx.closeIf();
}
ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
ctx.addLine(`let vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
if (ctx.parentNode) {
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;
+4 -1
View File
@@ -132,7 +132,7 @@ export function useStore(selector, options: SelectorOptions = {}): any {
__destroy.call(component, parent);
};
if (typeof result !== "object") {
if (typeof result !== "object" || result === null) {
return result;
}
return new Proxy(result, {
@@ -141,6 +141,9 @@ export function useStore(selector, options: SelectorOptions = {}): any {
},
set(target, k, v) {
throw new Error("Store state should only be modified through actions");
},
has(target, k) {
return k in result;
}
});
}
+17
View File
@@ -1,4 +1,5 @@
import { QWeb } from "./qweb/index";
import { registerSheet } from "./component/styles";
/**
* Owl Tags
@@ -25,3 +26,19 @@ export function xml(strings, ...args) {
QWeb.registerTemplate(name, value);
return name;
}
/**
* CSS tag helper for defining inline stylesheets. With this, one can simply define
* an inline stylesheet with just the following code:
* ```js
* class A extends Component {
* static style = css`.component-a { color: red; }`;
* }
* ```
*/
export function css(strings, ...args) {
const name = `__sheet__${QWeb.nextId++}`;
const value = String.raw(strings, ...args);
registerSheet(name, value);
return name;
}
+3 -6
View File
@@ -58,12 +58,9 @@ export function escape(str: string | number | undefined): string {
if (typeof str === "number") {
return String(str);
}
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&#x27;")
.replace(/`/g, "&#x60;");
const p = document.createElement("p");
p.textContent = str;
return p.innerHTML;
}
/**
+22 -25
View File
@@ -8,12 +8,11 @@ exports[`animations t-transition combined with component 1`] = `
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
var h = this.h;
let h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//COMPONENT
let k3 = \`__4__\`;
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
let vn1 = h('div', p1, c1);
// Component 'Child'
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();
@@ -30,9 +29,9 @@ exports[`animations t-transition combined with component 1`] = `
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k3] = w2.__owl__.id;
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {let finalize = () => {
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy();
};
delete w2.__owl__.transitionInserted;
@@ -53,13 +52,12 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
var h = this.h;
let h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let vn1 = h('div', p1, c1);
if (scope['state'].display) {
//COMPONENT
let k3 = \`__4__\`;
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
// Component 'Child'
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();
@@ -76,9 +74,9 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k3] = w2.__owl__.id;
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {let finalize = () => {
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy();
};
delete w2.__owl__.transitionInserted;
@@ -100,13 +98,12 @@ exports[`animations t-transition combined with t-component, remove and re-add be
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
var h = this.h;
let h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let vn1 = h('div', p1, c1);
if (scope['state'].flag) {
//COMPONENT
let k3 = \`__4__\`;
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
// Component 'Child'
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();
@@ -123,9 +120,9 @@ exports[`animations t-transition combined with t-component, remove and re-add be
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k3] = w2.__owl__.id;
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {let finalize = () => {
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy();
};
delete w2.__owl__.transitionInserted;
@@ -144,9 +141,9 @@ exports[`animations t-transition with no delay/duration 1`] = `
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
var h = this.h;
let h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
let vn1 = h('span', p1, c1);
p1.hook = {
insert: vn => {
utils.transitionInsert(vn, 'jupiler');
@@ -165,9 +162,9 @@ exports[`animations t-transition, on a simple node (insert) 1`] = `
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
var h = this.h;
let h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
let vn1 = h('span', p1, c1);
p1.hook = {
insert: vn => {
utils.transitionInsert(vn, 'chimay');
+6 -6
View File
@@ -254,11 +254,11 @@ describe("animations", () => {
widget.state.display = false;
patchNextFrame(cb => {
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="__4__">blue</span></div>'
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="__3__">blue</span></div>'
);
cb();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__4__">blue</span></div>'
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>'
);
def.resolve();
});
@@ -371,7 +371,7 @@ describe("animations", () => {
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__4__">blue</span></div>');
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__3__">blue</span></div>');
});
test("transitionInsert is called the correct amount of times", async () => {
@@ -405,14 +405,14 @@ describe("animations", () => {
widget.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__4__">blue</span></div>'
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>'
);
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__4__">blue</span></div>'
'<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__3__">blue</span></div>'
);
expect(QWeb.utils.transitionInsert).toBeCalledTimes(2);
@@ -423,7 +423,7 @@ describe("animations", () => {
expect(QWeb.utils.transitionInsert).toBeCalledTimes(3);
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__4__">blue</span></div>');
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__3__">blue</span></div>');
QWeb.utils.transitionInsert = oldTransitionInsert;
});
});
@@ -0,0 +1,156 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`class and style attributes with t-component dynamic t-att-style is properly added and updated on widget root el 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"ParentWidget\\"
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 'child'
const _4 = scope['state'].style;
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, undefined).then(()=>{if (w2.__owl__.isDestroyed) {return};w2.el.style=_4;});;
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.style = _4;}};});
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[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v2) 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"ParentWidget\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
context.__owl__.refs = context.__owl__.refs || {};
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Child'
const ref4 = \`child\`;
let _5 = {'a':true};
Object.assign(_5, {b:scope['state'].b})
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, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){}};});
let pvnode = h('dummy', {key: '__3__', hook: {insert(vn) {context.__owl__.refs[ref4] = w2;},remove() {},destroy(vn) {w2.destroy();delete context.__owl__.refs[ref4];}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.classObj=_5;
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v2) 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"Child\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _7 = {'c':true};
Object.assign(_7, utils.toObj({d:scope['state'].d}))
let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8);
return vn8;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v3) 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"ParentWidget\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
context.__owl__.refs = context.__owl__.refs || {};
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Child'
const ref4 = \`child\`;
let _5 = {'a':true};
Object.assign(_5, utils.toObj(scope['state'].b?'b':''))
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, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){}};});
let pvnode = h('dummy', {key: '__3__', hook: {insert(vn) {context.__owl__.refs[ref4] = w2;},remove() {},destroy(vn) {w2.destroy();delete context.__owl__.refs[ref4];}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.classObj=_5;
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v3) 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"Child\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _7 = {'c':true};
Object.assign(_7, utils.toObj(scope['state'].d?'d':''))
let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8);
return vn8;
}"
`;
File diff suppressed because it is too large Load Diff
@@ -8,12 +8,11 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
var h = this.h;
let h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//COMPONENT
let k3 = \`__4__\`;
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
let vn1 = h('div', p1, c1);
// Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {message:1};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
@@ -28,9 +27,9 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap[k3] = w2.__owl__.id;
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {w2.destroy();}}});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
@@ -0,0 +1,488 @@
// 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.create(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.create(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);
utils.getScope(scope, 'userdescr').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
+271
View File
@@ -0,0 +1,271 @@
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();
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
describe("class and style attributes with t-component", () => {
test("class is properly added on widget root el", async () => {
class Child extends Component<any, any> {
static template = xml`<div class="c"/>`;
}
class ParentWidget extends Component<any, any> {
static template = xml`<div><Child class="a b"/></div>`;
static components = { Child };
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><div class="c a b"></div></div>`);
});
test("empty class attribute is not added on widget root el", async () => {
class Child extends Component<any, any> {
static template = xml`<span/>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Child class=""/></div>`;
static components = { Child };
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><span></span></div>`);
});
test("t-att-class is properly added/removed on widget root el", async () => {
class Child extends Component<any, any> {
static template = xml`<div class="c"/>`;
}
class ParentWidget extends Component<any, any> {
static template = xml`<div><Child t-att-class="{a:state.a, b:state.b}"/></div>`;
static components = { Child };
state = useState({ a: true, b: false });
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><div class="c a"></div></div>`);
expect(QWeb.TEMPLATES[ParentWidget.template].fn.toString());
widget.state.a = false;
widget.state.b = true;
await nextTick();
expect(fixture.innerHTML).toBe(`<div><div class="c b"></div></div>`);
});
test("class with extra whitespaces", async () => {
env.qweb.addTemplate(
"ParentWidget",
`<div>
<Child class="a b c d"/>
</div>`
);
class Child extends Component<any, any> {}
class ParentWidget extends Component<any, any> {
static components = { Child };
}
env.qweb.addTemplate("Child", `<div/>`);
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><div class="a b c d"></div></div>`);
});
test("t-att-class is properly added/removed on widget root el (v2)", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget">
<Child class="a" t-att-class="{ b: state.b }" t-ref="child"/>
</div>
<span t-name="Child" class="c" t-att-class="{ d: state.d }"/>
</templates>`);
class Child extends Component<any, any> {
state = useState({ d: true });
}
class ParentWidget extends Component<any, any> {
static components = { Child };
state = useState({ b: true });
child = useRef("child");
}
const widget = new ParentWidget();
await widget.mount(fixture);
const span = fixture.querySelector("span")!;
expect(span.className).toBe("c d a b");
widget.state.b = false;
await nextTick();
expect(span.className).toBe("c d a");
(widget.child.comp as Child).state.d = false;
await nextTick();
expect(span.className).toBe("c a");
widget.state.b = true;
await nextTick();
expect(span.className).toBe("c a b");
(widget.child.comp as Child).state.d = true;
await nextTick();
expect(span.className).toBe("c a b d");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
});
test("t-att-class is properly added/removed on widget root el (v3)", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget">
<Child class="a" t-att-class="state.b ? 'b' : ''" t-ref="child"/>
</div>
<span t-name="Child" class="c" t-att-class="state.d ? 'd' : ''"/>
</templates>`);
class Child extends Component<any, any> {
state = useState({ d: true });
}
class ParentWidget extends Component<any, any> {
static components = { Child };
state = useState({ b: true });
child = useRef("child");
}
const widget = new ParentWidget();
await widget.mount(fixture);
const span = fixture.querySelector("span")!;
expect(span.className).toBe("c d a b");
widget.state.b = false;
await nextTick();
expect(span.className).toBe("c d a");
(widget.child.comp as Child).state.d = false;
await nextTick();
expect(span.className).toBe("c a");
widget.state.b = true;
await nextTick();
expect(span.className).toBe("c a b");
(widget.child.comp as Child).state.d = true;
await nextTick();
expect(span.className).toBe("c a b d");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
});
test("class on components do not interfere with user defined classes", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App" t-att-class="{ c: state.c }" />
</templates>`);
class App extends Component<any, any> {
state = useState({ c: true });
mounted() {
this.el!.classList.add("user");
}
}
const widget = new App();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe('<div class="c user"></div>');
widget.state.c = false;
await nextTick();
expect(fixture.innerHTML).toBe('<div class="user"></div>');
});
test("style is properly added on widget root el", async () => {
env.qweb.addTemplate(
"ParentWidget",
`
<div>
<t t-component="child" style="font-weight: bold;"/>
</div>`
);
class SomeComponent extends Component<any, any> {
static template = xml`<div/>`;
}
class ParentWidget extends Component<any, any> {
static components = { child: SomeComponent };
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><div style="font-weight: bold;"></div></div>`);
});
test("dynamic t-att-style is properly added and updated on widget root el", async () => {
env.qweb.addTemplate(
"ParentWidget",
`
<div>
<t t-component="child" t-att-style="state.style"/>
</div>`
);
class SomeComponent extends Component<any, any> {
static template = xml`<div/>`;
}
class ParentWidget extends Component<any, any> {
static components = { child: SomeComponent };
state = useState({ style: "font-size: 20px" });
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe(`<div><div style="font-size: 20px;"></div></div>`);
widget.state.style = "font-size: 30px";
await nextTick();
expect(fixture.innerHTML).toBe(`<div><div style="font-size: 30px;"></div></div>`);
});
test("error in subcomponent with class", async () => {
class Child extends Component<any, any> {
static template = xml`<div t-esc="this.will.crash"/>`;
}
class ParentWidget extends Component<any, any> {
static template = xml`<div><Child class="a"/></div>`;
static components = { Child };
}
const widget = new ParentWidget();
let error;
try {
await widget.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(fixture.innerHTML).toBe("");
});
});
File diff suppressed because it is too large Load Diff
+523
View File
@@ -0,0 +1,523 @@
import { Component, Env } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeTestEnv, makeTestFixture, 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();
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
describe("component error handling (catchError)", () => {
/**
* This test suite requires often to wait for 3 ticks. Here is why:
* - First tick is to let the app render and crash.
* - When we crash, we call the catchError handler in a setTimeout (because we
* need to wait for the previous rendering to be completely stopped). So, we
* need to wait for the second tick.
* - Then, when the handler changes the state, we need to wait for the interface
* to be rerendered.
* */
test("can catch an error in a component render function", async () => {
const consoleError = console.error;
console.error = jest.fn();
const handler = jest.fn();
env.qweb.on("error", null, handler);
class ErrorComponent extends Component<any, any> {
static template = xml`<div>hey<t t-esc="props.flag and state.this.will.crash"/></div>`;
}
class ErrorBoundary extends Component<any, any> {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component<any, any> {
static template = xml`
<div>
<ErrorBoundary><ErrorComponent flag="state.flag"/></ErrorBoundary>
</div>`;
state = useState({ flag: false });
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><div>heyfalse</div></div></div>");
app.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("no component catching error lead to full app destruction", async () => {
expect.assertions(6);
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Component<any, any> {
static template = xml`<div>hey<t t-esc="props.flag and state.this.will.crash"/></div>`;
}
class App extends Component<any, any> {
static template = xml`<div><ErrorComponent flag="state.flag"/></div>`;
static components = { ErrorComponent };
state = useState({ flag: false });
async render() {
try {
await super.render();
} catch (e) {
expect(e.message).toBe("Cannot read property 'this' of undefined");
}
}
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>heyfalse</div></div>");
app.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(app.__owl__.isDestroyed).toBe(true);
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Component<any, any> {
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;
}
class ErrorBoundary extends Component<any, any> {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component<any, any> {
static template = xml`
<div>
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>`;
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the initial call of a component render function (parent updated)", async () => {
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Component<any, any> {
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;
}
class ErrorBoundary extends Component<any, any> {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component<any, any> {
static template = xml`
<div>
<ErrorBoundary t-if="state.flag"><ErrorComponent /></ErrorBoundary>
</div>`;
state = useState({ flag: false });
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
app.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the constructor call of a component render function", async () => {
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">Some text</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Component<any, any> {
constructor(parent) {
super(parent);
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Component<any, any> {
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component<any, any> {
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the willStart call", async () => {
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Component<any, any> {
static template = xml`<div t-name="ErrorComponent">Some text</div>`;
async willStart() {
// we wait a little bit to be in a different stack frame
await nextTick();
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Component<any, any> {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component<any, any> {
static template = xml`<div><ErrorBoundary><ErrorComponent /></ErrorBoundary></div>`;
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test.skip("can catch an error in the mounted call", async () => {
// we do not catch error in mounted anymore
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">Some text</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Component<any, any> {
mounted() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Component<any, any> {
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component<any, any> {
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
});
test.skip("can catch an error in the willPatch call", async () => {
// we do not catch error in willPatch anymore
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Component<any, any> {
static template = xml`<div><t t-esc="props.message"/></div>`;
willPatch() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Component<any, any> {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component<any, any> {
static template = xml`
<div>
<span><t t-esc="state.message"/></span>
<ErrorBoundary><ErrorComponent message="state.message" /></ErrorBoundary>
</div>`;
state = useState({ message: "abc" });
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>abc</span><div><div>abc</div></div></div>");
app.state.message = "def";
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>def</span><div>Error handled</div></div>");
expect(console.error).toHaveBeenCalledTimes(1);
console.error = consoleError;
});
test("a rendering error will reject the mount promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
// we do not catch error in willPatch anymore
class App extends Component<any, any> {
static template = xml`<div><t t-esc="this.will.crash"/></div>`;
}
const app = new App();
let error;
try {
await app.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("an error in mounted call will reject the mount promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class App extends Component<any, any> {
static template = xml`<div>abc</div>`;
mounted() {
throw new Error("boom");
}
}
const app = new App();
let error;
try {
await app.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("an error in willPatch call will reject the render promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class App extends Component<any, any> {
static template = xml`<div><t t-esc="val"/></div>`;
val = 3;
willPatch() {
throw new Error("boom");
}
}
const app = new App();
await app.mount(fixture);
app.val = 4;
let error;
try {
await app.render();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("an error in patched call will reject the render promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class App extends Component<any, any> {
static template = xml`<div><t t-esc="val"/></div>`;
val = 3;
patched() {
throw new Error("boom");
}
}
const app = new App();
await app.mount(fixture);
app.val = 4;
let error;
try {
await app.render();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("a rendering error in a sub component will reject the mount promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
// we do not catch error in willPatch anymore
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="this.will.crash"/></div>`;
}
class App extends Component<any, any> {
static template = xml`<div><Child/></div>`;
static components = { Child };
}
const app = new App();
let error;
try {
await app.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("a rendering error will reject the render promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
// we do not catch error in willPatch anymore
class App extends Component<any, any> {
static template = xml`<div><t t-if="flag" t-esc="this.will.crash"/></div>`;
flag = false;
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>");
app.flag = true;
let error;
try {
await app.render();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("a rendering error will reject the render promise (with sub components)", async () => {
class Child extends Component<any, any> {
static template = xml`<span></span>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Child/><t t-esc="x.y"/></div>`;
static components = { Child };
}
let error;
try {
const parent = new Parent();
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'y' of undefined");
});
});
+876
View File
@@ -0,0 +1,876 @@
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();
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>");
});
});
+170
View File
@@ -0,0 +1,170 @@
import { Component, Env } from "../../src/component/component";
import { processSheet } from "../../src/component/styles";
import { xml, css } from "../../src/tags";
import { makeTestFixture, makeTestEnv } 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: an Env, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
document.head.innerHTML = "";
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("styles and component", () => {
test("can define an inline stylesheet", async () => {
class App extends Component<any, any> {
static template = xml`<div class="app">text</div>`;
static style = css`
.app {
color: red;
}
`;
}
expect(document.head.innerHTML).toBe("");
const app = new App();
expect(document.head.innerHTML).toBe(`<style component=\"App\">.app {
color: red;
}</style>`);
await app.mount(fixture);
const style = getComputedStyle(app.el!);
expect(style.color).toBe("red");
expect(fixture.innerHTML).toBe('<div class="app">text</div>');
});
test("inherited components properly apply css", async () => {
class App extends Component<any, any> {
static template = xml`<div class="app">text</div>`;
static style = css`
.app {
color: red;
}
`;
}
class SubApp extends App {
static style = css`
.app {
font-weight: bold;
}
`;
}
expect(document.head.innerHTML).toBe("");
const app = new SubApp();
expect(document.head.innerHTML).toBe(`<style component=\"SubApp\">.app {
font-weight: bold;
}</style><style component=\"App\">.app {
color: red;
}</style>`);
await app.mount(fixture);
const style = getComputedStyle(app.el!);
expect(style.color).toBe("red");
expect(style.fontWeight).toBe("bold");
expect(fixture.innerHTML).toBe('<div class="app">text</div>');
});
test("get a meaningful error message if css helper is missing", async () => {
class App extends Component<any, any> {
static template = xml`<div class="app">text</div>`;
static style = `.app {color: red;}`;
}
let error;
try {
new App();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(
"Invalid css stylesheet for component 'App'. Did you forget to use the 'css' tag helper?"
);
});
test("inline stylesheets are processed", async () => {
class App extends Component<any, any> {
static template = xml`<div class="app">text</div>`;
static style = css`
.app {
color: red;
.some-class {
font-weight: bold;
width: 40px;
}
display: block;
}
`;
}
new App();
expect(document.head.querySelector("style")!.innerHTML).toBe(`.app {
color: red;
}
.app .some-class {
font-weight: bold;
width: 40px;
}
.app {
display: block;
}`);
});
test("properly handle rules with commas", async () => {
const sheet = processSheet(`.parent-a, .parent-b {
.child-a, .child-b {
color: red;
}
}`);
expect(sheet)
.toBe(`.parent-a .child-a, .parent-a .child-b, .parent-b .child-a, .parent-b .child-b {
color: red;
}`);
});
test("handle & selector", async () => {
let sheet = processSheet(`.btn {
&.danger {
color: red;
}
}`);
expect(sheet).toBe(`.btn.danger {
color: red;
}`);
sheet = processSheet(`.some-class {
&.btn {
.other-class ~ & {
color: red;
}
}
}`);
expect(sheet).toBe(`.other-class ~ .some-class.btn {
color: red;
}`);
});
});
+409
View File
@@ -0,0 +1,409 @@
import { Component, Env } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeDeferred, makeTestEnv, makeTestFixture, 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();
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
describe("mount targets", () => {
test("can attach a component to an existing node (if same tagname)", async () => {
class App extends Component<any, any> {
static template = xml`<div>app</div>`;
}
const div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
await app.mount(div, { position: "self" });
expect(fixture.innerHTML).toBe("<div>app</div>");
});
test("cannot attach a component to an existing node (if not same tagname)", async () => {
class App extends Component<any, any> {
static template = xml`<span>app</span>`;
}
const div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
let error;
try {
await app.mount(div, { position: "self" });
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot attach 'App' to target node (not same tag name)");
});
test("can mount a component (with position='first-child')", async () => {
class App extends Component<any, any> {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture, { position: "first-child" });
expect(fixture.innerHTML).toBe("<div>app</div><span></span>");
});
test("can mount a component (with position='last-child')", async () => {
class App extends Component<any, any> {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture, { position: "last-child" });
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
test("default mount option is 'last-child'", async () => {
class App extends Component<any, any> {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
});
describe("unmounting and remounting", () => {
test("widget can be unmounted and remounted", async () => {
const steps: string[] = [];
class MyWidget extends Component<any, any> {
static template = xml`<div>Hey</div>`;
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
patched() {
throw new Error("patched should not be called");
}
}
const w = new MyWidget();
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]);
w.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual(["willstart", "mounted", "willunmount"]);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted", "willunmount", "mounted"]);
});
test("widget can be mounted twice without ill effect", async () => {
const steps: string[] = [];
class MyWidget extends Component<any, any> {
static template = xml`<div>Hey</div>`;
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
}
const w = new MyWidget();
await w.mount(fixture);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]);
});
test("state changes in willUnmount do not trigger rerender", async () => {
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`
<span><t t-esc="props.val"/><t t-esc="state.n"/></span>
`;
state = useState({ n: 2 });
__render(f) {
steps.push("render");
return super.__render(f);
}
willPatch() {
steps.push("willPatch");
}
patched() {
steps.push("patched");
}
willUnmount() {
steps.push("willUnmount");
this.state.n = 3;
}
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<Child t-if="state.flag" val="state.val"/>
</div>
`;
static components = { Child };
state = useState({ val: 1, flag: true });
}
const widget = new Parent();
await widget.mount(fixture);
expect(steps).toEqual(["render"]);
expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
widget.state.flag = false;
await nextTick();
// we make sure here that no call to __render is done
expect(steps).toEqual(["render", "willUnmount"]);
});
test("state changes in willUnmount will be applied on remount", async () => {
class TestWidget extends Component<any, any> {
static template = xml`
<div><t t-esc="state.val"/></div>
`;
state = useState({ val: 1 });
willUnmount() {
this.state.val = 3;
}
}
const widget = new TestWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>1</div>");
widget.unmount();
expect(fixture.innerHTML).toBe("");
await nextTick(); // wait for changes to be detected before remounting
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3</div>");
// we want to make sure that there are no remaining tasks left at this point.
expect(Component.scheduler.tasks.length).toBe(0);
});
test("sub component is still active after being unmounted and remounted", async () => {
class Child extends Component<any, any> {
static template = xml`
<p t-on-click="state.value++">
<t t-esc="state.value"/>
</p>`;
state = useState({ value: 1 });
}
class Parent extends Component<any, any> {
static components = { Child };
static template = xml`<div><Child/></div>`;
}
const w = new Parent();
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
fixture.querySelector("p")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>2</p></div>");
w.unmount();
await nextTick();
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>2</p></div>");
fixture.querySelector("p")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>3</p></div>");
});
test("change state just before mounting component", async () => {
const steps: number[] = [];
class TestWidget extends Component<any, any> {
static template = xml`
<div><t t-esc="state.val"/></div>
`;
state = useState({ val: 1 });
__render(f) {
steps.push(this.state.val);
return super.__render(f);
}
}
TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render);
const widget = new TestWidget();
widget.state.val = 2;
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>2</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1);
// unmount and re-mount, as in this case, willStart won't be called, so it's
// slightly different
widget.unmount();
widget.state.val = 3;
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(2);
expect(steps).toEqual([2, 3]);
});
test("change state while mounting component", async () => {
const steps: number[] = [];
class TestWidget extends Component<any, any> {
static template = xml`
<div><t t-esc="state.val"/></div>
`;
state = useState({ val: 1 });
__render(f) {
steps.push(this.state.val);
return super.__render(f);
}
}
TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render);
TestWidget.prototype.__patch = jest.fn(TestWidget.prototype.__patch);
const widget = new TestWidget();
let prom = widget.mount(fixture);
widget.state.val = 2;
await prom;
expect(fixture.innerHTML).toBe("<div>2</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1);
// unmount and re-mount, as in this case, willStart won't be called, so it's
// slightly different
widget.unmount();
prom = widget.mount(fixture);
widget.state.val = 3;
await prom;
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(3);
expect(TestWidget.prototype.__patch).toHaveBeenCalledTimes(2);
expect(steps).toEqual([2, 2, 3]);
});
test("change state while component is unmounted", async () => {
let child;
class Child extends Component<any, any> {
static template = xml`<span t-esc="state.val"/>`;
state = useState({
val: "C1"
});
constructor(parent, props) {
super(parent, props);
child = this;
}
}
class Parent extends Component<any, any> {
static components = { Child };
static template = xml`<div><t t-esc="state.val"/><Child/></div>`;
state = useState({ val: "P1" });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div>P1<span>C1</span></div>");
parent.unmount();
expect(fixture.innerHTML).toBe("");
parent.state.val = "P2";
child.state.val = "C2";
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div>P2<span>C2</span></div>");
});
test("unmount component during a re-rendering", async () => {
const def = makeDeferred();
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
willUpdateProps() {
return def;
}
}
Child.prototype.__render = jest.fn(Child.prototype.__render);
class Parent extends Component<any, any> {
static template = xml`<div><Child val="state.val"/></div>`;
static components = { Child };
state = useState({ val: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
expect(Child.prototype.__render).toBeCalledTimes(1);
parent.state.val = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
parent.unmount();
expect(fixture.innerHTML).toBe("");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(Child.prototype.__render).toBeCalledTimes(1);
});
test("widget can be mounted on different target", async () => {
const steps: string[] = [];
class MyWidget extends Component<any, any> {
static template = xml`<div>Hey</div>`;
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
patched() {
throw new Error("patched should not be called");
}
}
const div = document.createElement("div");
const span = document.createElement("span");
fixture.appendChild(div);
fixture.appendChild(span);
const w = new MyWidget();
await w.mount(div);
expect(fixture.innerHTML).toBe("<div><div>Hey</div></div><span></span>");
await w.mount(span);
expect(fixture.innerHTML).toBe("<div></div><span><div>Hey</div></span>");
});
});
+5 -6
View File
@@ -1,4 +1,4 @@
import { Env } from "../src/component/component";
import { Env, Component } from "../src/component/component";
import { scheduler } from "../src/component/scheduler";
import { EvalContext, QWeb } from "../src/qweb/qweb";
import { CompilationContext } from "../src/qweb/compilation_context";
@@ -25,6 +25,7 @@ beforeEach(() => {
slots = Object.assign({}, QWeb.slots);
nextId = QWeb.nextId;
TEMPLATES = Object.assign({}, QWeb.TEMPLATES);
Component.scheduler.tasks = [];
});
afterEach(() => {
@@ -32,6 +33,7 @@ afterEach(() => {
QWeb.slots = slots;
QWeb.nextId = nextId;
QWeb.TEMPLATES = TEMPLATES;
Component.scheduler.tasks = [];
});
// helpers
@@ -121,11 +123,8 @@ export function renderToString(
context: EvalContext = {},
extra?: any
): string {
const node = renderToDOM(qweb, t, context, extra);
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");
}
const result = qweb.renderToString(t, context, extra);
expect(qweb.templates[t].fn.toString()).toMatchSnapshot();
return result;
}
+37 -1
View File
@@ -9,7 +9,8 @@ import {
onWillPatch,
onWillStart,
onWillUpdateProps,
useSubEnv
useSubEnv,
useExternalListener
} from "../src/hooks";
import { xml } from "../src/tags";
@@ -592,4 +593,39 @@ describe("hooks", () => {
expect(fixture.innerHTML).toBe("<div><span>2</span></div>");
expect(steps).toEqual(["onWillStart", "onWillUpdateProps"]);
});
test("useExternalListener", async () => {
let n = 0;
class MyComponent extends Component<any, any> {
static template = xml`<span><t t-esc="props.value"/></span>`;
constructor(parent, props) {
super(parent, props);
useExternalListener(window as any, "click", this.increment);
}
increment() {
n++;
}
}
class App extends Component<any, any> {
static template = xml`<div><MyComponent t-if="state.flag"/></div>`;
static components = { MyComponent };
state = useState({ flag: false });
}
const app = new App();
await app.mount(fixture);
expect(n).toBe(0);
window.dispatchEvent(new Event("click"));
expect(n).toBe(0);
app.state.flag = true;
await nextTick();
window.dispatchEvent(new Event("click"));
expect(n).toBe(1);
app.state.flag = false;
await nextTick();
window.dispatchEvent(new Event("click"));
expect(n).toBe(1);
});
});
File diff suppressed because it is too large Load Diff
+142 -14
View File
@@ -120,9 +120,11 @@ describe("t-esc", () => {
expect(renderToString(qweb, "test", { var: "ok" })).toBe("<span>ok</span>");
});
test.skip("escaping", () => {
test("escaping", () => {
qweb.addTemplate("test", `<span><t t-esc="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<ok>" })).toBe("<span>&lt;ok&gt;</span>");
expect(renderToString(qweb, "test", { var: "<ok>abc</ok>" })).toBe(
"<span>&amp;lt;ok&amp;gt;abc&amp;lt;/ok&amp;gt;</span>"
);
});
test("escaping on a node", () => {
@@ -139,17 +141,49 @@ describe("t-esc", () => {
qweb.addTemplate("test", `<span t-esc="var">nope</span>`);
expect(renderToString(qweb, "test")).toBe("<span>nope</span>");
});
test("t-esc is escaped", () => {
qweb.addTemplate("test", `<div><t t-set="var"><p>escaped</p></t><t t-esc="var"/></div>`);
const domRendered = renderToDOM(qweb, "test");
expect(domRendered.textContent).toBe("<p>escaped</p>");
});
test("t-esc=0 is escaped", () => {
qweb.addTemplate("test", `<span><t t-esc="0"/></span>`);
qweb.addTemplate("testCaller", `<div><t t-call="test"><p>escaped</p></t></div>`);
const domRendered = renderToDOM(qweb, "testCaller") as HTMLElement;
expect(domRendered.querySelector("span")!.textContent).toBe("<p>escaped</p>");
});
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", () => {
@@ -256,6 +290,18 @@ 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", () => {
qweb.addTemplate(
"test",
@@ -318,12 +364,13 @@ describe("t-set", () => {
<t t-set="v1" t-value="'after'"/>
<t t-set="v3" t-value="true"/>
<t t-raw="v2"/>
</div>`);
</div>`
);
expect(renderToString(qweb, "test")).toBe("<div><span>before</span></div>");
});
expect(renderToString(qweb, "test")).toBe("<div><span>before</span></div>");
});
test("t-set with t-value (truthy) and body", () => {
test("t-set with t-value (truthy) and body", () => {
qweb.addTemplate(
"test",
`<div>
@@ -335,10 +382,11 @@ describe("t-set", () => {
<t t-set="v1" t-value="'after'"/>
<t t-set="v3" t-value="false"/>
<t t-raw="v2"/>
</div>`);
</div>`
);
expect(renderToString(qweb, "test")).toBe("<div>Truthy</div>");
});
expect(renderToString(qweb, "test")).toBe("<div>Truthy</div>");
});
});
describe("t-if", () => {
@@ -426,7 +474,7 @@ describe("t-if", () => {
});
test("t-esc with t-elif", () => {
qweb.addTemplate("test", `<div><t t-if="false">abc</t><t t-else="1" t-esc="'x'"/></div>`);
qweb.addTemplate("test", `<div><t t-if="false">abc</t><t t-else="" t-esc="'x'"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>x</div>");
});
@@ -618,7 +666,7 @@ describe("attributes", () => {
expect(result).toBe(`<div foo="a 0 is 1 of 2 ]"></div>`);
});
test.skip("various escapes", () => {
test("various escapes", () => {
// not needed??
qweb.addTemplate(
"test",
@@ -634,7 +682,7 @@ describe("attributes", () => {
baz: 1,
qux: { qux: "<>" }
});
const expected = `<div foo="&lt;foo" bar="&lt;bar&gt;" baz="&lt;&quot;&lt;baz&gt;&quot;&gt;" qux="&lt;&gt;"></div>`;
const expected = '<div foo="<foo" bar="0" baz="<1>" qux="<>"></div>';
expect(result).toBe(expected);
});
@@ -880,6 +928,23 @@ describe("t-call (template calling", () => {
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>
@@ -1272,6 +1337,69 @@ describe("t-on", () => {
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 () => {
expect.assertions(7);
qweb.addTemplate(
@@ -1644,7 +1772,7 @@ describe("debugging", () => {
console.log = jest.fn();
qweb.addTemplate(
"test",
`<div t-debug="1"><t t-if="true"><span t-debug="1">hey</span></t></div>`
`<div t-debug=""><t t-if="true"><span t-debug="">hey</span></t></div>`
);
qweb.render("test");
expect(qweb.templates.test.fn.toString()).toMatchSnapshot();
@@ -1658,7 +1786,7 @@ describe("debugging", () => {
console.log = jest.fn();
qweb.addTemplates(`
<templates>
<p t-name="sub" t-debug="1">coucou</p>
<p t-name="sub" t-debug="">coucou</p>
<div t-name="test">
<t t-call="sub"/>
</div>
+26
View File
@@ -50,6 +50,18 @@ describe("tokenizer", () => {
{ type: "OPERATOR", value: "typeof " },
{ type: "SYMBOL", value: "a" }
]);
expect(tokenize("a...1")).toEqual([
{ type: "SYMBOL", value: "a" },
{ type: "OPERATOR", value: "..." },
{ type: "VALUE", value: "1" }
]);
expect(tokenize("a in b")).toEqual([
{ type: "SYMBOL", value: "a" },
{ type: "OPERATOR", value: "in " },
{ type: "SYMBOL", value: "b" }
]);
});
test("strings", () => {
@@ -128,6 +140,7 @@ describe("expression evaluation", () => {
expect(compileExpr("color === 'black'", {})).toBe("scope['color']==='black'");
expect(compileExpr("'li_'+item", {})).toBe("'li_'+scope['item']");
expect(compileExpr("state.val > 1", {})).toBe("scope['state'].val>1");
expect(compileExpr("a in b", {})).toBe("scope['a']in scope['b']");
});
test("boolean operations", () => {
@@ -170,4 +183,17 @@ describe("expression evaluation", () => {
"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']])");
});
});
+9 -9
View File
@@ -6,17 +6,17 @@ exports[`Link component can render simple cases 1`] = `
// Template name: \\"__template__1\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
var h = this.h;
let _5 = utils.toObj({'router-link-active':scope['isActive']});
var _6 = scope['href'];
let c7 = [], p7 = {key:7,attrs:{href: _6},class:_5,on:{}};
var vn7 = h('a', p7, c7);
extra.handlers['click' + 7] = extra.handlers['click' + 7] || function (e) {if (!context.__owl__.isMounted){return}const fn = context['navigate'];if (fn) { fn.call(context, e); } else { context.navigate; }};
p7.on['click'] = extra.handlers['click' + 7];
let h = this.h;
let _4 = utils.toObj({'router-link-active':scope['isActive']});
let _5 = scope['href'];
let c6 = [], p6 = {key:6,attrs:{href: _5},class:_4,on:{}};
let vn6 = h('a', p6, c6);
extra.handlers['click__7__'] = extra.handlers['click__7__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['navigate'](e);};
p6.on['click'] = extra.handlers['click__7__'];
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot8) {
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context}));
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c6, parent: extra.parent || context}));
}
return vn7;
return vn6;
}"
`;
@@ -9,35 +9,37 @@ exports[`RouteComponent can render simple cases 1`] = `
let parent = context;
let scope = Object.create(context);
let result;
var h = this.h;
let h = this.h;
if (scope['routeComponent']) {
const nodeKey5 = scope['env'].router.currentRouteName;
//COMPONENT
let k7 = \`__8__\` + nodeKey5;
let w6 = k7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k7]] : false;
let vn9 = {};
result = vn9;
let props6 = Object.assign({}, scope['env'].router.currentParams);
if (w6 && w6.__owl__.currentFiber && !w6.__owl__.vnode) {
w6.destroy();
w6 = false;
{
let key0 = scope['env'].router.currentRouteName;
// Component 'routeComponent'
let k5 = \`__5__\${key0}__\`;
let w4 = k5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k5]] : false;
let vn6 = {};
result = vn6;
let props4 = Object.assign({}, scope['env'].router.currentParams);
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
w4.destroy();
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 (w6) {
w6.__updateProps(props6, extra.fiber, undefined);
let pvnode = w6.__owl__.pvnode;
utils.defineProxy(vn9, pvnode);
} else {
let componentKey6 = \`routeComponent\`;
let W6 = context.constructor.components[componentKey6] || QWeb.components[componentKey6]|| scope['routeComponent'];
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
w6 = new W6(parent, props6);
parent.__owl__.cmap[k7] = w6.__owl__.id;
let fiber = w6.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k7, hook: {remove() {},destroy(vn) {w6.destroy();}}});
utils.defineProxy(vn9, pvnode);
w6.__owl__.pvnode = pvnode;
}
w6.__owl__.parentLastFiberId = extra.fiber.id;
}
return result;
}"
+47 -1
View File
@@ -49,10 +49,13 @@ describe("connecting a component to store", () => {
});
test("useStore can observe primitive types and call onUpdate", async () => {
const state = { isBoolean: false };
const state = { isBoolean: false, nullValue: null };
const actions = {
setTrue({ state }) {
state.isBoolean = true;
},
setNotNull({ state }) {
state.nullValue = "ok";
}
};
const store = new Store({ state, actions });
@@ -61,8 +64,10 @@ describe("connecting a component to store", () => {
static template = xml`
<div>
<span t-if="isBoolean">ok</span>
<span t-if="nullValue !== null">not null</span>
</div>`;
isBoolean: boolean;
nullValue: string;
constructor() {
super();
this.isBoolean = useStore(state => state.isBoolean, {
@@ -70,6 +75,11 @@ describe("connecting a component to store", () => {
this.isBoolean = isBoolean;
}
});
this.nullValue = useStore(state => state.nullValue, {
onUpdate: nullValue => {
this.nullValue = nullValue;
}
});
}
}
@@ -82,6 +92,42 @@ describe("connecting a component to store", () => {
store.dispatch("setTrue");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>ok</span></div>");
store.dispatch("setNotNull");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>ok</span><span>not null</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 () => {
+57
View File
@@ -0,0 +1,57 @@
/**
* 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 sub component with non stringifiable props", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.obj.val"/></span>`;
}
const circularObject: any = { val: 1 };
circularObject.circularObject = circularObject;
class Parent extends Component<any, any> {
static template = xml`<div><Child obj="obj"/></div>`;
static components = { Child };
obj = circularObject;
}
const parent = new Parent(null, {});
await parent.mount(fixture);
parent.unmount();
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=<JSON error>",
"[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",
"[OWL_DEBUG] Parent<id=1> willUnmount",
"[OWL_DEBUG] Child<id=2> willUnmount"
]);
console.log = log;
});
+154
View File
@@ -0,0 +1,154 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = useState({ counter: 0 });
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
Message.components = { Counter };
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log");
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 < messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
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.logRef.el.appendChild(div);
this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
}
clearLog() {
this.logRef.el.innerHTML = "";
}
}
App.components = { Message };
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadFile("templates.xml");
App.env = {
qweb: new owl.QWeb({ templates })
};
const app = new App();
app.mount(document.body);
}
start();
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL v1.0.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<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-model="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-model="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">
<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>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+6 -1
View File
@@ -35,7 +35,12 @@
});
function toStr(obj) {
let str = JSON.stringify(obj || {});
let str;
try {
str = JSON.stringify(obj || {});
} catch (e) {
str = "<JSON error>";
}
if (str.length > 200) {
str = str.slice(0, 200) + "...";
}
+1
View File
@@ -37,6 +37,7 @@
<li><a href="benchmarks/owl-0.21.0">OWL 0.21.0</a></li>
<li><a href="benchmarks/owl-0.24.0">OWL 0.24.0</a></li>
<li><a href="benchmarks/owl-1.0.0-beta1">OWL 1.0.0-beta1</a></li>
<li><a href="benchmarks/owl-1.0.0">OWL 1.0.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul>
<ul>
+56 -9
View File
@@ -455,7 +455,7 @@ const actions = {
todo.completed = !todo.completed;
},
clearCompleted({ state, dispatch }) {
for (let todo of state.todos) {
for (let todo of state.todos.slice()) {
if (todo.completed) {
dispatch("removeTodo", todo.id);
}
@@ -1089,20 +1089,20 @@ const RESPONSIVE_XML = `<templates>
<button>Button!</button>
</div>
<t t-name="maincontent">
<FormView />
<Chatter />
</t>
<div t-name="App" class="app" t-att-class="{mobile: env.isMobile, desktop: !env.isMobile}">
<t t-set="maincontent">
<FormView />
<Chatter />
</t>
<Navbar/>
<ControlPanel/>
<div class="content-wrapper" t-if="!env.isMobile">
<div class="content">
<t t-raw="maincontent"/>
<t t-call="maincontent"/>
</div>
</div>
<t t-else="1">
<t t-raw="maincontent"/>
<t t-else="">
<t t-call="maincontent"/>
</t>
</div>
</templates>
@@ -1438,7 +1438,7 @@ const FORM_XML = `<templates>
<div>Text: <t t-esc="state.text"/></div>
<div>Other Text: <t t-esc="state.othertext"/></div>
<div>Number: <t t-esc="state.number"/></div>
<div>Boolean: <t t-if="state.bool">True</t><t t-else="1">False</t></div>
<div>Boolean: <t t-if="state.bool">True</t><t t-else="">False</t></div>
<div>Color: <t t-esc="state.color"/></div>
</div>
</templates>
@@ -1811,6 +1811,49 @@ const WMS_CSS = `body {
font-size: 20px;
}`;
const SFC = `// This example illustrates how Owl enables single file components,
// which include code, template and style.
//
// This is very useful in some situations, such as testing or quick prototyping.
// Note that this example has no external xml or css file, everything is
// contained in a single js file.
const { Component, useState, tags } = owl;
const { xml, css } = tags;
// Counter component
const COUNTER_TEMPLATE = xml\`
<button t-on-click="state.value++">
Click! [<t t-esc="state.value"/>]
</button>\`;
const COUNTER_STYLE = css\`
button {
color: blue;
}\`;
class Counter extends Component {
state = useState({ value: 0})
}
Counter.template = COUNTER_TEMPLATE;
Counter.style = COUNTER_STYLE;
// App
const APP_TEMPLATE = xml\`
<div>
<Counter/>
<Counter/>
</div>\`;
class App extends Component {}
App.template = APP_TEMPLATE;
App.components = { Counter };
// Application setup
const app = new App();
app.mount(document.body);
`;
export const SAMPLES = [
{
description: "Components",
@@ -1823,6 +1866,10 @@ export const SAMPLES = [
code: FORM,
xml: FORM_XML
},
{
description: "Single File Components",
code: SFC
},
{
description: "Animations",
code: ANIMATION,
+215
View File
@@ -0,0 +1,215 @@
const package = require("../package.json");
const readline = require("readline");
const fs = require("fs");
const exec = require("child_process").exec;
const chalk = require("chalk");
const GitHub = require("github-api");
const REL_NOTES_FILE = `release-notes.md`;
const STEPS = 10;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
startRelease().then(() => {
rl.close();
});
// -----------------------------------------------------------------------------
// Relase Script
// -----------------------------------------------------------------------------
async function startRelease() {
log(`*** Owl release script ***`);
log(`Current Version: ${package.version}`);
// ---------------------------------------------------------------------------
log(`Step 1/${STEPS}: collecting info...`);
const current = package.version;
const next = await ask("Next version: ");
let file = await ask(`Release notes (${REL_NOTES_FILE}): `);
file = file || REL_NOTES_FILE;
let content;
try {
content = await readFile("./" + file);
} catch (e) {
logSubContent(e.message);
log("Cannot find release notes... Aborting");
return;
}
// Todo: add playground update feature
// let shouldUpdateStr = await ask("Update Playground? (y/n)");
// const shouldUpdatePlayground = shouldUpdateStr === "y";
const token = await ask("Github token: ");
// ---------------------------------------------------------------------------
log(`Step 2/${STEPS}: running tests...`);
const testsResult = await execCommand("npm run test");
if (testsResult !== 0) {
log("Test suite does not pass. Aborting.");
return;
}
ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 3/${STEPS}: updating package.json, readme.md and roadmap.md...`);
await replaceInFile("./package.json", current, next);
await replaceInFile("./README.md", current, next);
await replaceInFile("./roadmap.md", current, next);
ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 4/${STEPS}: creating git commit...`);
const gitResult = await execCommand(`git commit -am "[REL] v${next}\n\n${content}"`);
if (gitResult !== 0) {
log("Git commit failed. Aborting.");
return;
}
ask("Ready for next step...");
// -------------------j--------------------------------------------------------
log(`Step 5/${STEPS}: building owl (iife version)...`);
const buildResult = await execCommand("npm run build");
if (buildResult !== 0) {
log("Build failed. Aborting.");
return;
}
ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 6/${STEPS}: minifying owl...`);
const minifyResult = await execCommand("npm run minify");
if (minifyResult !== 0) {
log("Minify failed. Aborting.");
return;
}
ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 7/${STEPS}: pushing on github...`);
const pushResult = await execCommand("git push");
if (pushResult !== 0) {
log("git push failed. Aborting.");
return;
}
ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 8/${STEPS}: publishing release notes on github...`);
const options = {
tag_name: `v${next}`,
name: `v${next}`,
body: content,
draft: true // todo: remove this someday
};
const result = await createRelease(token, options);
console.log(result);
ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 9/${STEPS}: adding assets to release...`);
await ask("Please add owl.js and owl.min.js to draft release, then confirm");
// todo: do this with curl
// curl \
// -H "Authorization: token $GITHUB_TOKEN" \
// -H "Content-Type: $(file -b --mime-type $FILE)" \
// --data-binary @$FILE \
// "https://uploads.github.com/repos/hubot/singularity/releases/123/assets?name=$(basename $FILE)"
// ---------------------------------------------------------------------------
log(`Step 10/${STEPS}: publishing module on npm...`);
await execCommand("npm run publish");
log("Owl Release process completed! Thank you for your patience");
}
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
function log(text) {
console.log(chalk.yellow(formatLog(text)));
}
function formatLog(text) {
return `[REL] ${text}`;
}
function logSubContent(text) {
for (let line of text.split("\n")) {
if (line.trim()) {
console.log(" " + line);
}
}
}
function ask(question) {
return new Promise(resolve => {
rl.question(question, result => {
resolve(result);
});
});
}
function logStream(stream) {
stream.on("data", data => {
logSubContent(data);
});
}
function execCommand(command) {
return new Promise(resolve => {
const childProcess = exec(command, (err, stdout, stderr) => {
if (err) {
resolve(err.code);
}
});
childProcess.on("exit", code => {
resolve(code);
});
logStream(childProcess.stdout);
logStream(childProcess.stderr);
});
}
function readFile(file) {
return new Promise((resolve, reject) => {
fs.readFile(file, "utf8", function(err, content) {
if (err) {
reject(err);
} else {
resolve(content);
}
});
});
}
async function replaceInFile(file, from, to) {
const content = await readFile(file);
return new Promise((resolve, reject) => {
const updatedContent = content.replace(new RegExp(from, "g"), to);
fs.writeFile(file, updatedContent, "utf8", err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
function createRelease(token, options) {
return new Promise((resolve, reject) => {
var gh = new GitHub({ token });
gh.getRepo("odoo", "owl").createRelease(options, (err, result, req) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}
+2 -1
View File
@@ -13,7 +13,8 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strictPropertyInitialization": true,
"strictNullChecks": true
"strictNullChecks": true,
"declarationDir": "dist/types"
},
"include": ["src/**/*.ts","src/*.ts"]
}