Compare commits

...

18 Commits

Author SHA1 Message Date
Géry Debongnie 609c108607 [REL] bump to v0.15.0 2019-06-17 15:59:15 +02:00
Aaron Bohy a56939f13a [IMP] store: memoize getters
Closes #95
2019-06-17 15:46:48 +02:00
Géry Debongnie 5ff501f8b8 [IMP] component: add support for default slot
closes #180
2019-06-17 09:26:37 +02:00
Géry Debongnie 60917bb102 [DOC] improve main readme.md 2019-06-15 10:38:29 +02:00
Géry Debongnie b201ef8162 [FIX] component: crashes if no template can be found
closes #178
2019-06-14 23:33:45 +02:00
Géry Debongnie 9cacc78ad7 [DOC] component: add section on root widget 2019-06-14 23:11:23 +02:00
Géry Debongnie 0d933b8570 [IMP] tools: improve playground samples
closes #177
2019-06-14 21:44:12 +02:00
Aaron Bohy 72f6daa695 [FIX] playground: t-async leftover in sample 2019-06-14 21:03:21 +02:00
Géry Debongnie 567d78a9ac [DOC] component: put input bindings section higher 2019-06-14 15:46:14 +02:00
Géry Debongnie f013c57050 [REF] component: rename t-async into t-asyncroot 2019-06-14 15:40:32 +02:00
Géry Debongnie b9fae87273 [DOC] doc: miscellaneous formatting updates 2019-06-14 15:33:43 +02:00
Géry Debongnie 1f0457f69b [REF] tools: use t-model in benchmarks 2019-06-14 15:01:29 +02:00
Géry Debongnie c481a73a76 [ADD] component: implement t-model directive
closes #170
2019-06-14 15:01:29 +02:00
Aaron Bohy e64e415b3b [IMP] qweb: add t-async directive on t-widget
Closes #98
2019-06-14 14:58:24 +02:00
Géry Debongnie aa406b53e2 [FIX] tools: react benchmarks did not look like the others 2019-06-14 09:36:29 +02:00
Géry Debongnie 57027bd0fa [FIX] doc: add missing link 2019-06-13 16:02:35 +02:00
Aaron Bohy 671c662fee [FIX] playground: don't remove empty tabs
Fixes #169
2019-06-13 14:04:59 +02:00
Géry Debongnie 0ae70b1d63 [ADD] tools: add v0.14.0 benchmark 2019-06-13 12:22:38 +02:00
31 changed files with 5467 additions and 452 deletions
+25 -20
View File
@@ -10,7 +10,7 @@ other Odoo related projects. OWL's main feature is a _declarative component syst
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the OWL framework.
## OWL's design principles
## OWL's Design Principles
OWL is designed to be used in highly dynamic applications where changing
requirements are common, code needs to be maintained by large teams.
@@ -30,12 +30,20 @@ Owl is not designed to be fast or small (even though it is quite good on those
two topics). If you are interested in a comparison with React or Vue, you will
find some more information [here](doc/comparison.md).
# Example
## Example
Here is a short example to illustrate interactive widgets:
```xml
<templates>
<button t-name="Counter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
</templates>
```
```javascript
class ClickCounter extends owl.Component {
class Counter extends owl.Component {
state = { value: 0 };
increment() {
@@ -43,36 +51,33 @@ class ClickCounter extends owl.Component {
}
}
const TEMPLATES = `
<button t-name="ClickCounter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
const qweb = new owl.QWeb(TEMPLATES);
const counter = new ClickCounter({ qweb });
const counter = new Counter({ qweb });
counter.mount(document.body);
```
More interesting examples can be found on the [playground](https://odoo.github.io/owl/playground) application.
Note that we assume here that the xml templates are available in the `TEMPLATES`
string. More interesting examples can be found on the
[playground](https://odoo.github.io/owl/playground) application.
## Installing/Building
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-0.14.0.js](https://github.com/odoo/owl/releases/download/v0.14.0/owl.js)
- [owl-0.14.0.min.js](https://github.com/odoo/owl/releases/download/v0.14.0/owl.min.js)
- [owl-0.15.0.js](https://github.com/odoo/owl/releases/download/v0.15.0/owl.js)
- [owl-0.15.0.min.js](https://github.com/odoo/owl/releases/download/v0.15.0/owl.min.js)
Some npm scripts are available:
| Command | Description |
| ---------------------- | ------------------------------------------------------------------------------- |
| `npm install` | install every dependency required for this project |
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
| `npm run minify` | minify the prebuilt owl.js file |
| `npm run test` | run all tests |
| `npm run test:watch` | run all tests, and keep a watcher |
| Command | Description |
| --------------------- | ---------------------------------------------------------------------------- |
| `npm install` | install every dependency required for this project |
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
| `npm run minify` | minify the prebuilt owl.js file |
| `npm run test` | run all tests |
| `npm run test:watch` | run all tests, and keep a watcher |
| `npm run tools` | build tools applications, start a static server (see [here](doc/tooling.md)) |
| `npm run tools:watch` | same as `tools`, but with a watcher to rebuild owl |
| `npm run tools:watch` | same as `tools`, but with a watcher to rebuild owl |
## Documentation
+8 -6
View File
@@ -7,6 +7,8 @@ In this page, we try to highlight some of these differences. Obviously, some
effort was done to be fair. However, if you disagree with some of the points
discussed, feel free to open an issue/submit a PR to correct this text.
## Content
- [Size](#size)
- [Tooling/Build Step](#toolingbuild-step)
- [Templating](#templating)
@@ -14,7 +16,7 @@ discussed, feel free to open an issue/submit a PR to correct this text.
- [Reactiveness](#reactiveness)
- [State Management](#state-management)
### Size
## Size
OWL is intended to be small and to work at a slightly lower level of abstraction
than React and Vue. Also, jQuery is not the same kind of framework, but it is interesting to compare.
@@ -26,7 +28,7 @@ than React and Vue. Also, jQuery is not the same kind of framework, but it is in
| React + ReactDOM + Redux | | 40kb |
| jQuery | 86kb | 30kb |
### Tooling/Build step
## Tooling/Build step
OWL is designed to be easy to use in a standalone way. For various reasons,
Odoo does not want to rely on standard web tools (such as webpack), and OWL can
@@ -84,7 +86,7 @@ This has the advantage of having the full power of Javascript, but is less
structured than a template language. Note that the tooling is quite impressive:
there is a syntax highlighter for jsx here on github!
### Asynchronous rendering
## Asynchronous Rendering
This is actually a big difference between OWL and React/Vue: components in OWL
are totally asynchronous. They have two asynchronous hooks in their lifecycle:
@@ -115,7 +117,7 @@ extremely powerful as well, as demonstrated by the Odoo Web Client.
Lazy loading static libraries can obviously be done with React/Vue, but it is
more convoluted.
### Reactiveness
## Reactiveness
React has a simple model: whenever the state changes, it is
replaced with a new state (via the setState method). Then, the DOM is patched.
@@ -129,7 +131,7 @@ Owl is closer to vue: it also tracks magically the state properties, but it does
only increment a counter whenever it changes (and a _deep_ counter for each of
its parents). This assumes that the state is actually a tree.
### State Management
## State Management
Managing the state of an application is a tricky issue. Many solutions have
been proposed these last few years. It also depends on the kind of application we
@@ -190,5 +192,5 @@ keeps track of who get data, and retrigger a render when it was changed.
Owl store is a little bit like a mix of redux and vuex: it has mutations and
actions, like VueX, it keeps track of the state changes, but it does not notify
a component when the state changes. Instead, components need to connect to the
a component when the state changes. Instead, components need to connect to the
store like in redux, with a function that will listen to the relevant state.
+166 -16
View File
@@ -4,6 +4,7 @@
- [Overview](#overview)
- [Example](#example)
- [Root Widget And Environment](#root-widget-and-environment)
- [Reference](#reference)
- [Properties](#properties)
- [Static Properties](#static-properties)
@@ -11,12 +12,14 @@
- [Lifecycle](#lifecycle)
- [Composition](#composition)
- [Event Handling](#event-handling)
- [`t-key` directive](#t-key-directive)
- [`t-mounted` directive](#t-mounted-directive)
- [Form Input Bindings](#form-input-bindings)
- [`t-key` Directive](#t-key-directive)
- [`t-mounted` Directive](#t-mounted-directive)
- [Semantics](#semantics)
- [Props Validation](#props-validation)
- [Keeping References](#keeping-references)
- [References](#references)
- [Slots](#slots)
- [Asynchronous rendering](#asynchronous-rendering)
- [Asynchronous Rendering](#asynchronous-rendering)
## Overview
@@ -73,6 +76,30 @@ a state object is defined. It is not mandatory to use the state object, but it
is certainly encouraged. The state object is [observed](observer.md), and any
change to it will cause a rerendering.
## Root Widget And Environment
Most of the time, Owl widget will be created automatically by the `t-widget`
directive in a template. There is however an obvious exception: the root widget
of an Owl application has to be created manually:
```js
class App extends owl.Component { ... }
const qweb = new owl.QWeb(TEMPLATES);
const env = { qweb: qweb };
const app = new App(env);
app.mount(document.body);
```
The root widget needs an environment. In Owl, an environment is an object with
a `qweb` key, which has to be a [QWeb](qweb.md) instance. This qweb instance will
be used to render everything.
The environment will be given to each child, unchanged, in the `env` property.
This can be very useful to share common information/methods. For example, all
rpcs can be made through a `rpc` method in the environment. This makes it very
easy to test a component.
## Reference
An Owl component is a small class which represent a widget or some UI element.
@@ -308,7 +335,7 @@ the DOM. This is a good place to remove some listeners, for example.
This is the opposite method of `mounted`.
## Composition
### Composition
The example above shows a QWeb template with a `t-on-click` directive. Widget
templates are standard [QWeb](qweb.md) templates, but with an extra directive:
@@ -407,7 +434,7 @@ with a class object:
<t t-widget="MyWidget" t-att-class="{a: state.flagA, b: state.flagB}" />
```
### Event handling
### Event Handling
In a component's template, it is useful to be able to register handlers on some
elements to some specific events. This is what makes a template _alive_. There
@@ -493,7 +520,99 @@ The `t-on` directive also allows to prebind some arguments. For example,
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
from the rendering context.
### `t-key` directive
### Form Input Bindings
It is very common to need to be able to read the value out of an html `input` (or
`textarea`, or `select`) in order to use it (note: it does not need to be in a
form!). A possible way to do this is to do it by hand:
```js
class Form extends owl.Component {
state = { text: "" };
_updateInputValue(event) {
this.state.text = event.target.value;
}
}
```
```xml
<div>
<input t-on-input="_updateInputValue"/>
<span t-esc="state.text"/>
</div>
```
This works. However, this requires a little bit of _plumbing_ code. Also, the
plumbing code is slightly different if you need to interact with a checkbox,
or with radio buttons, or with select tags.
To help with this situation, Owl has a builtin directive `t-model`: its value
is the (top-level) name in the state object. With the `t-model` directive, we
can write a shorter code, equivalent to the previous example:
```js
class Form extends owl.Component {
state = { text: "" };
}
```
```xml
<div>
<input t-model="text"/>
<span t-esc="state.text"/>
</div>
```
The `t-model` directive works with `<input>`, `<input type="checkbox">`,
`<input type="radio">`, `<textarea>` and `<select>`:
```xml
<div>
<div>Text in an input: <input t-model="someVal"/></div>
<div>Textarea: <textarea t-model="otherVal"/></div>
<div>Boolean value: <input type="checkbox" t-model="someFlag"/></div>
<div>Selection:
<select t-model="color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
<div>
Selection with radio buttons:
<span>
<input type="radio" name="color" id="red" value="red" t-model="color"/>
<label for="red">Red</label>
</span>
<span>
<input type="radio" name="color" id="blue" value="blue" t-model="color"/>
<label for="blue">Blue</label>
</span>
</div>
</div>
```
Like event handling, the `t-model` directive accepts some modifiers:
| Modifier | Description |
| --------- | -------------------------------------------------------------------- |
| `.lazy` | update the value on the `change` event (default is on `input` event) |
| `.number` | tries to parse the value to a number (using `parseFloat`) |
| `.trim` | trim the resulting value |
For example:
```xml
<input t-model.lazy="someVal"/>
```
These modifiers can be combined. For instance, `t-model.lazy.number` will only
update a number whenever the change is done.
Note: the online playground has an example to show how it works.
### `t-key` Directive
Even though Owl tries to be as declarative as possible, some DOM state is still
locked inside the DOM: for example, the scrolling state, the current user selection,
@@ -517,7 +636,7 @@ There are three main use cases:
- _animations_: give a different identity to a component. Ex: thread id with
animations on add/remove message.
### `t-mounted` directive
### `t-mounted` Directive
The `t-mounted` directive allows to register a callback to execute whenever the node
is inserted into the DOM.
@@ -709,7 +828,7 @@ Examples:
};
```
### Keeping references
### References
The `t-ref` directive helps a component keep reference to some inside part of it.
Like the `t-on` directive, it can work either on a DOM node, or on a component:
@@ -751,12 +870,12 @@ this.refs.widget_44;
### Slots
To make generic components, it is useful to be able for a parent widget to *inject*
some sub template, but still be the owner. For example, a generic dialog widget
To make generic components, it is useful to be able for a parent widget to _inject_
some sub template, but still be the owner. For example, a generic dialog widget
will need to render some content, some footer, but with the parent as the
rendering context.
This is what *slots* are for.
This is what _slots_ are for.
```xml
<div t-name="Dialog" class="modal">
@@ -787,11 +906,11 @@ Slots are defined by the caller, with the `t-set` directive:
```
In this example, the widget `Dialog` will render the slots `content` and `footer`
with its parent as rendering context. This means that clicking on the button
with its parent as rendering context. This means that clicking on the button
will execute the `doSomething` method on the parent, not on the dialog.
Warning! Slots have a technical constraint: the result of the slot rendering
should have exactly one root node. So,
should have exactly one root node. So,
```xml
<t t-set="content">
@@ -809,7 +928,22 @@ is not allowed. A workaround could be to wrap the content in a div:
</div>
```
### Asynchronous rendering
Default slot: the first element inside the widget which is not a named slot will
be considered the `default` slot. For example:
```xml
<div t-name="Parent">
<t t-widget="Child">
<span>some content</span>
</t>
</div>
<div t-name="Child">
<t t-slot="default"/>
</div>
```
### Asynchronous Rendering
Working with asynchronous code always adds a lot of complexity to a system. Whenever
different parts of a system are active at the same time, one needs to think
@@ -827,9 +961,25 @@ There are two different common problems with Owl asynchronous rendering model:
Here are a few tips on how to work with asynchronous widgets:
1. minimize the use of asynchronous widgets!
1. Minimize the use of asynchronous widgets!
2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
synchronous renderings
3. Lazy loading external libraries is a good use case for async rendering. This
is mostly fine, because we can assume that it will only takes a fraction of a
second, and only once (see `owl.utils.loadJS`)
4. For all the other cases, the `t-asyncroot` directive (to use alongside
`t-widget`) is there to help you. When this directive is met, a new rendering
sub tree is created, such that the rendering of that component (and its
children) is not tied to the rendering of the rest of the interface. It can
be used on an asynchronous component, to prevent it from delaying the
rendering of the whole interface, or on a synchronous one, such that its
rendering isn't delayed by other (asynchronous) components. Note that this
directive has no effect on the first rendering, but only on subsequent ones
(triggered by state or props changes).
```xml
<div t-name="ParentWidget">
<t t-widget="SyncChild"/>
<t t-widget="AsyncChild" t-asyncroot="1"/>
</div>
```
+16 -16
View File
@@ -30,21 +30,21 @@ In those situations, we need a way to tell the observer that something happened.
This can be done by using the `set` and `delete` (only for objects) static
methods of the `Observer`.
```javascript
const observer = new owl.Observer();
const obj = { a: 1 };
observer.observe(obj);
obj.b = 2; // won't notify the change
owl.Observer.set(obj, "b", 2); // will notify the change
```javascript
const observer = new owl.Observer();
const obj = { a: 1 };
observer.observe(obj);
obj.b = 2; // won't notify the change
owl.Observer.set(obj, "b", 2); // will notify the change
delete obj.b; // won't notify the change
owl.Observer.delete(obj, "b"); // will notify the change
```
delete obj.b; // won't notify the change
owl.Observer.delete(obj, "b"); // will notify the change
```
```javascript
const observer = new owl.Observer();
const arr = ["a"];
observer.observe(arr);
arr[0] = "b"; // won't notify the change
owl.Observer.set(arr, 0, "b"); // will notify the change
```
```javascript
const observer = new owl.Observer();
const arr = ["a"];
observer.observe(arr);
arr[0] = "b"; // won't notify the change
owl.Observer.set(arr, 0, "b"); // will notify the change
```
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 Quick Start 🦉
## Static server
## Static Server
Let us assume that we have a static server running somewhere. We could then
simply add an html page with a few extra files.
+20 -19
View File
@@ -6,14 +6,14 @@
- [Directives](#directives)
- [QWeb Engine](#qweb-engine)
- [Reference](#reference)
- [White spaces](#white-spaces)
- [Root nodes](#root-nodes)
- [Expression evaluation](#expression-evaluation)
- [Static html nodes](#static-html-nodes)
- [Outputting data](#outputting-data)
- [White Spaces](#white-spaces)
- [Root Nodes](#root-nodes)
- [Expression Evaluation](#expression-evaluation)
- [Static html Nodes](#static-html-nodes)
- [Outputting Data](#outputting-data)
- [Setting Variables](#setting-variables)
- [Conditionals](#conditionals)
- [Dynamic attributes](#dynamic-attributes)
- [Dynamic Attributes](#dynamic-attributes)
- [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates)
- [Debugging](#debugging)
@@ -65,15 +65,16 @@ We present here a list of all standard QWeb directives:
The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives:
| Name | Description |
| ------------------------- | --------------------------------------------------------------------------------------- |
| `t-widget`, `t-keepalive` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#keeping-references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
| `t-slot` | [Rendering a slot](component.md#slots) |
| Name | Description |
| -------------------------------------------- | ----------------------------------------------------------------------------------- |
| `t-widget`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
| `t-slot` | [Rendering a slot](component.md#slots) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
## QWeb Engine
@@ -140,7 +141,7 @@ We define in this section the specification of how `QWeb` templates should be
rendered. Note that we only document here the standard QWeb specification. Owl
specific extensions are documented in various other parts of the documentation.
### White spaces
### White Spaces
White spaces in a templates are handled in a special way:
@@ -148,7 +149,7 @@ White spaces in a templates are handled in a special way:
- if a whitespace-only text node contains a linebreak, it is ignored
- the previous rules do not apply if we are in a `<pre>` tag
### Root nodes
### Root Nodes
For many reasons, Owl QWeb templates should have a single root node. More
precisely, the result of a template rendering should have a single root node:
@@ -174,7 +175,7 @@ Note: this does not apply to subtemplates (see the `t-call` directive). In that
case, they will be inlined in the main template, and can actually have many
root nodes.
### Expression evaluation
### Expression Evaluation
QWeb expressions are strings that will be processed at compile time. Each variable in
the javascript expression will be replaced by a lookup in the context (so, the
@@ -344,7 +345,7 @@ Extra conditional branching directives `t-elif` and `t-else` are also available:
</div>
```
### Dynamic attributes
### Dynamic Attributes
One can use the `t-att-` directive to add dynamic attributes. Its main use is to
evaluate an expression (at rendering time) and bind an attribute to its result:
+1 -2
View File
@@ -1,6 +1,5 @@
# 🦉 OWL Documentation 🦉
## Reference
- [Animations](animations.md)
@@ -12,7 +11,7 @@
- [Utils](utils.md)
- [Virtual DOM](vdom.md)
## Learning resources
## Learning Resources
- [Quick Start](quick_start.md)
+75 -69
View File
@@ -9,7 +9,7 @@
- [Mutations](#mutations)
- [Actions](#actions)
- [Getters](#getters)
- [Connecting a component](#connecting-a-component)
- [Connecting a Component](#connecting-a-component)
## Overview
@@ -32,46 +32,45 @@ Here is what a simple store looks like:
```js
const actions = {
addTodo({commit}, message) {
commit('addTodo', message);
}
addTodo({ commit }, message) {
commit("addTodo", message);
}
};
const mutations = {
addTodo({state}, message) {
const todo = {
id: state.nextId++,
message,
isCompleted: false,
};
state.todos.push(todo);
},
addTodo({ state }, message) {
const todo = {
id: state.nextId++,
message,
isCompleted: false
};
state.todos.push(todo);
}
};
const state = {
todos: [],
nextId: 1,
todos: [],
nextId: 1
};
const store = new owl.Store({state, actions, mutations});
store.on('update', () => console.log(store.state));
const store = new owl.Store({ state, actions, mutations });
store.on("update", () => console.log(store.state));
// updating the state
store.dispatch('addTodo', 'fix all bugs');
store.dispatch("addTodo", "fix all bugs");
```
## Reference
The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events whenever its
state is changed. Note that these events are triggered only after a microtask
state is changed. Note that these events are triggered only after a microtask
tick, so only one event will be triggered for any number of state changes in a
call stack.
Also, it is important to mention that the state is observed (with an `owl.Observer`),
which is the reason why it is able to know if it was changed. This implies that
which is the reason why it is able to know if it was changed. This implies that
state changes need to be done carefully in some cases (adding a new key to an
object, or modifying an array with the `arr[i] = newValue` syntax). See the
object, or modifying an array with the `arr[i] = newValue` syntax). See the
[Observer](observer.md)'s documentation for more details.
### Public API
@@ -82,26 +81,26 @@ object, or modifying an array with the `arr[i] = newValue` syntax). See the
### Mutations
Mutations are the only way to modify the state. Changing the state outside a
mutation is not allowed (and should throw an error). Mutations are synchronous.
Mutations are the only way to modify the state. Changing the state outside a
mutation is not allowed (and should throw an error). Mutations are synchronous.
### Actions
Actions are used to coordinate state changes. It is also useful whenever some
asynchronous logic is necessary. For example, fetching data should be done
Actions are used to coordinate state changes. It is also useful whenever some
asynchronous logic is necessary. For example, fetching data should be done
in an action.
```js
const actions = {
async login({commit}) {
commit('setLoginState', 'pending');
try {
const loginInfo = await doSomeRPC('/login/', 'someinfo');
commit('setLoginState', loginInfo);
} catch {
commit('setLoginState', 'error');
}
async login({ commit }) {
commit("setLoginState", "pending");
try {
const loginInfo = await doSomeRPC("/login/", "someinfo");
commit("setLoginState", loginInfo);
} catch {
commit("setLoginState", "error");
}
}
};
```
@@ -128,23 +127,28 @@ transform the data contained in the store.
```js
const getters = {
getPost({state}, id) {
const post = state.posts.find(p => p.id === id);
const author = state.authors.find(a => a.id = post.id);
return {
id,
author,
content: post.content
};
},
getPost({ state }, id) {
const post = state.posts.find(p => p.id === id);
const author = state.authors.find(a => (a.id = post.id));
return {
id,
author,
content: post.content
};
}
};
// somewhere else
const post = store.getters.getPost(id);
```
### Connecting a component
Getters take *at most* one argument.
Note that getters are cached if they don't take any argument, or their argument
is a string or a number.
### Connecting a Component
By default, an Owl `Component` is not connected to any store. The `connect`
function is there to create sub Components that are connected versions of
@@ -152,34 +156,35 @@ Components.
```javascript
const actions = {
increment({commit}) {
commit('increment', 1);
}
increment({ commit }) {
commit("increment", 1);
}
};
const mutations = {
increment({state}, val) {
state.counter += val;
}
increment({ state }, val) {
state.counter += val;
}
};
const state = {
counter: 0,
counter: 0
};
const store = new owl.Store({state, actions, mutations});
const store = new owl.Store({ state, actions, mutations });
class Counter extends owl.Component {
increment() {
this.env.store.dispatch('increment');
}
increment() {
this.env.store.dispatch("increment");
}
}
function mapStoreToProps(state) {
return {
value: state.counter
};
return {
value: state.counter
};
}
const ConnectedCounter = owl.connect(Counter, mapStoreToProps);
const counter = new ConnectedCounter({ store, qweb });
```
```xml
<button t-name="Counter" t-on-click="increment">
Click Me! [<t t-esc="props.value"/>]
@@ -187,17 +192,18 @@ const counter = new ConnectedCounter({ store, qweb });
```
The arguments of `connect` are:
- `Counter`: an owl `Component` to connect
- `mapStoreToProps`: a function that extracts the `props` of the Component
from the `state` of the `Store` and returns them as a dict
- `options`: dictionary of optional parameters that may contain
- `getStore`: a function that takes the `env` in arguments and returns an
instance of `Store` to connect to (if not given, connects to `env.store`)
- `hashFunction`: the function to use to detect changes in the state (if not
given, generates a function that uses revision numbers, incremented at
each state change)
- `deep`: [only useful if no hashFunction is given] if false, only watch
for top level state changes (true by default)
- `Counter`: an owl `Component` to connect
- `mapStoreToProps`: a function that extracts the `props` of the Component
from the `state` of the `Store` and returns them as a dict
- `options`: dictionary of optional parameters that may contain
- `getStore`: a function that takes the `env` in arguments and returns an
instance of `Store` to connect to (if not given, connects to `env.store`)
- `hashFunction`: the function to use to detect changes in the state (if not
given, generates a function that uses revision numbers, incremented at
each state change)
- `deep`: [only useful if no hashFunction is given] if false, only watch
for top level state changes (true by default)
The `connect` function returns a sub class of the given `Component` which is
connected to the `store`.
+16 -16
View File
@@ -18,34 +18,35 @@ not ready yet, resolved directly otherwise). If called with a callback as
argument, it executes it as soon as the DOM ready (or directly).
```js
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function ([templates]) {
const qweb = new owl.QWeb(templates);
const app = new App({ qweb });
app.mount(document.body);
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([
templates
]) {
const qweb = new owl.QWeb(templates);
const app = new App({ qweb });
app.mount(document.body);
});
```
```js
owl.utils.whenReady(function() {
const qweb = new owl.QWeb();
const app = new App({ qweb });
app.mount(document.body);
const qweb = new owl.QWeb();
const app = new App({ qweb });
app.mount(document.body);
});
```
## `loadJS`
`loadJS` takes a url (string) for a javascript resource, and loads it. It returns
a promise, so the caller can properly react when it is ready. Also, it is smart:
a promise, so the caller can properly react when it is ready. Also, it is smart:
it maintains a list of urls previously loaded (or currently being loaded), and
prevent doing twice the work.
```js
class MyComponent extends owl.Component {
willStart() {
return owl.utils.loadJS('/static/libs/someLib.js');
}
willStart() {
return owl.utils.loadJS("/static/libs/someLib.js");
}
}
```
@@ -53,13 +54,12 @@ class MyComponent extends owl.Component {
```js
async function makeEnv() {
const templates = await owl.utils.loadTemplates('templates.xml');
const qweb = new owl.QWeb(templates);
return { qweb };
const templates = await owl.utils.loadTemplates("templates.xml");
const qweb = new owl.QWeb(templates);
return { qweb };
}
```
## `escape`
## `debounce`
+1 -4
View File
@@ -1,11 +1,10 @@
# 🦉 VDom 🦉
Owl is a declarative component system: we declare the structure of the component
tree, and Owl will translate that to a list of imperative operations. This
tree, and Owl will translate that to a list of imperative operations. This
translation is done by a virtual dom. This is the low level layer of Owl, most
developer will not need to call directly the virtual dom functions.
The main idea behind a virtual dom is to keep a in-memory representation of the
DOM (called a virtual node), and whenever some change is needed, to regenerate
a new representation, compute the difference between the old and the new, then
@@ -17,5 +16,3 @@ apply the changes.
- `patch`: compare two virtual nodes, and apply the difference.
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "owl",
"version": "0.14.0",
"version": "0.15.0",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"scripts": {
+27 -15
View File
@@ -274,20 +274,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
// we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called.
const patchLen = patchQueue!.length;
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue![i];
patch.push(patch[0].willPatch());
}
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue![i];
patch[0]._patch(patch[1]);
}
for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue![i];
patch[0].patched(patch[2]);
}
this._applyPatchQueue(<any[]>patchQueue);
}
}
@@ -471,7 +458,9 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
p = p.__proto__;
}
if (p === Component) {
this.template = "default";
throw new Error(
`Could not find template for component "${this.constructor.name}"`
);
} else {
tmap[name] = template;
this.template = template;
@@ -568,6 +557,29 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
return <Props>props;
}
/**
* Apply the given patch queue. A patch is a pair [c, vn], where c is a
* Component instance and vn a VNode.
* 1) Call 'willPatch' on the component of each patch
* 2) Call '_patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in inverse order
*/
_applyPatchQueue(patchQueue: any[]) {
const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch.push(patch[0].willPatch());
}
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch[0]._patch(patch[1]);
}
for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i];
patch[0].patched(patch[2]);
}
}
/**
* Validate the component props (or next props) against the (static) props
* description. This is potentially an expensive operation: it may needs to
+4 -4
View File
@@ -149,7 +149,6 @@ export class QWeb {
if (data) {
this.addTemplates(data);
}
this.addTemplate("default", "<div></div>");
}
static addDirective(directive: Directive) {
@@ -362,7 +361,7 @@ export class QWeb {
for (let i = 0; i < attributes.length; i++) {
let attrName = attributes[i].name;
if (attrName.startsWith("t-")) {
let dName = attrName.slice(2).split("-")[0];
let dName = attrName.slice(2).split(/-|\./)[0];
if (!(dName in DIRECTIVE_NAMES)) {
throw new Error(`Unknown QWeb directive: '${attrName}'`);
}
@@ -379,12 +378,13 @@ export class QWeb {
const name = attributes[j].name;
if (
name === "t-" + directive.name ||
name.startsWith("t-" + directive.name + "-")
name.startsWith("t-" + directive.name + "-") ||
name.startsWith("t-" + directive.name + ".")
) {
fullName = name;
value = attributes[j].textContent;
validDirectives.push({ directive, value, fullName });
if (directive.name === "on") {
if (directive.name === "on" || directive.name === 'model') {
withHandlers = true;
}
}
+96 -22
View File
@@ -13,6 +13,7 @@ import { VNode } from "./vdom";
* - t-widget/t-keepalive
* - t-mounted
* - t-slot
* - t-model
*/
//------------------------------------------------------------------------------
@@ -225,13 +226,6 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* // t-widget directive: we start by evaluating the expression given by t-key:
* let key5 = "somestring";
*
* // We keep the index of the position of the widget in the closure. We push
* // null to reserve the slot, and will replace it later by the widget vnode,
* // when it will be ready (do not forget that preparing/rendering a widget is
* // asynchronous)
* let _2_index = c1.length;
* c1.push(null);
*
* // def3 is the promise that will contain later either the new widget
* // creation, or the props update...
* let def3;
@@ -246,6 +240,13 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* ? context.__owl__.children[context.__owl__.cmap[key5]]
* : false;
*
* // We keep the index of the position of the widget in the closure. We push
* // null to reserve the slot, and will replace it later by the widget vnode,
* // when it will be ready (do not forget that preparing/rendering a widget is
* // asynchronous)
* let _2_index = c1.length;
* c1.push(null);
*
* // we evaluate here the props given to the component. It is done here to be
* // able to easily reference it later, and also, it might be an expensive
* // computation, so it is certainly better to do it only once
@@ -360,7 +361,7 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
QWeb.addDirective({
name: "widget",
extraNames: ["props", "keepalive"],
extraNames: ["props", "keepalive", "asyncroot"],
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//WIDGET");
@@ -368,6 +369,7 @@ QWeb.addDirective({
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
let async = node.getAttribute("t-asyncroot") ? true : false;
// t-on- events and t-transition
const events: [string, string[], string, string][] = [];
@@ -413,8 +415,6 @@ QWeb.addDirective({
// want to evaluate it only once)
ctx.addLine(`let key${keyID} = ${key};`);
}
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
ctx.addLine(`c${ctx.parentNode}.push(null);`);
ctx.addLine(`let def${defID};`);
let templateID = key
? `key${keyID}`
@@ -505,6 +505,17 @@ QWeb.addDirective({
ctx.addLine(
`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`
);
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
if (async) {
ctx.addLine(`const patchQueue${widgetID} = [];`);
ctx.addLine(
`c${
ctx.parentNode
}.push(w${widgetID} && w${widgetID}.__owl__.pvnode || null);`
);
} else {
ctx.addLine(`c${ctx.parentNode}.push(null);`);
}
ctx.addLine(`let props${widgetID} = {${propStr}};`);
ctx.addIf(
`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode`
@@ -536,22 +547,30 @@ QWeb.addDirective({
);
// SLOTS
const slotNodes = node.querySelectorAll("[t-set]");
if (slotNodes.length) {
if (node.childElementCount) {
const clone = <Element>node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]");
const slotId = qweb.nextSlotId++;
for (let i = 0, length = slotNodes.length; i < length; i++) {
const slotNode = slotNodes[i];
const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set");
const slotFn = qweb._compile(`slot_${key}_template`, slotNode);
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
}
ctx.addLine(`w${widgetID}.__owl__.slotId = ${slotId};`);
if (slotNodes.length) {
for (let i = 0, length = slotNodes.length; i < length; i++) {
const slotNode = slotNodes[i];
slotNode.parentElement!.removeChild(slotNode);
const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set");
const slotFn = qweb._compile(`slot_${key}_template`, slotNode);
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
}
}
if (clone.childElementCount) {
const content = clone.children[0];
const slotFn = qweb._compile(`slot_default_template`, content);
qweb.slots[`${slotId}_default`] = slotFn.bind(qweb);
}
}
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
// FIXME: click to re-add widget during remove transition -> leak
ctx.addLine(
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeWidgetCode}}}});c${
ctx.parentNode
@@ -560,8 +579,9 @@ QWeb.addDirective({
ctx.addElse();
// need to update widget
const patchQueueCode = async ? `patchQueue${widgetID}` : "extra.patchQueue";
ctx.addLine(
`def${defID} = def${defID} || w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`
`def${defID} = def${defID} || w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, ${patchQueueCode});`
);
let keepAliveCode = "";
if (keepAlive) {
@@ -576,7 +596,13 @@ QWeb.addDirective({
);
ctx.closeIf();
ctx.addLine(`extra.promises.push(def${defID});`);
if (async) {
ctx.addLine(
`def${defID}.then(w${widgetID}._applyPatchQueue.bind(w${widgetID}, patchQueue${widgetID}));`
);
} else {
ctx.addLine(`extra.promises.push(def${defID});`);
}
if (
node.hasAttribute("t-if") ||
@@ -646,3 +672,51 @@ QWeb.addDirective({
return true;
}
});
//------------------------------------------------------------------------------
// t-model
//------------------------------------------------------------------------------
UTILS.toNumber = function(val: string): number | string {
const n = parseFloat(val);
return isNaN(n) ? val : n;
};
QWeb.addDirective({
name: "model",
priority: 42,
atNodeCreation({ ctx, nodeID, value, node, fullName }) {
const type = node.getAttribute("type");
let handler;
let event = fullName.includes(".lazy") ? "change" : "input";
if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
event = "change";
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
} else if (type === "checkbox") {
ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`);
handler = `(ev) => {context.state['${value}'] = ev.target.checked}`;
} else if (type === "radio") {
const nodeValue = node.getAttribute("value")!;
ctx.addLine(
`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`
);
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
event = "click";
} else {
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
let valueCode = `ev.target.value${trimCode}`;
if (fullName.includes(".number")) {
ctx.rootContext.shouldDefineUtils = true;
valueCode = `utils.toNumber(${valueCode})`;
}
handler = `(ev) => {context.state['${value}'] = ${valueCode}}`;
}
ctx.addLine(
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
);
ctx.addLine(
`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`
);
}
});
+14 -1
View File
@@ -47,6 +47,7 @@ export class Store extends EventBus {
env: any;
observer: Observer;
getters: { [name: string]: (payload?) => any };
_gettersCache: { [name: string]: {} };
constructor(config: StoreConfig, options: StoreOption = {}) {
super();
@@ -56,19 +57,31 @@ export class Store extends EventBus {
this.mutations = config.mutations;
this.env = config.env;
this.observer = new Observer();
this.observer.notifyCB = this.trigger.bind(this, "update");
this.observer.notifyCB = () => {
this._gettersCache = {};
this.trigger("update");
};
this.observer.allowMutations = false;
this.observer.observe(this.state);
this.getters = {};
this._gettersCache = {};
if (this.debug) {
this.history.push({ state: this.state });
}
const cTypes = ["undefined", "number", "string"];
for (let entry of Object.entries(config.getters || {})) {
const name: string = entry[0];
const func: (...any) => any = entry[1];
this.getters[name] = payload => {
if (this._commitLevel === 0 && cTypes.indexOf(typeof payload) >= 0) {
this._gettersCache[name] = this._gettersCache[name] || {};
this._gettersCache[name][payload] =
this._gettersCache[name][payload] ||
func({ state: this.state, getters: this.getters }, payload);
return this._gettersCache[name][payload];
}
return func({ state: this.state, getters: this.getters }, payload);
};
}
+4 -4
View File
@@ -10,10 +10,10 @@ exports[`animations t-transition combined with t-widget 1`] = `
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -54,10 +54,10 @@ exports[`animations t-transition combined with t-widget and t-if 1`] = `
var vn1 = h('div', p1, c1);
if (context['state'].display) {
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
+420 -30
View File
@@ -1,5 +1,248 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`async rendering delayed t-widget with t-asyncroot directive 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('button', p2, c2);
c1.push(vn2);
if (!context['updateApp']) {
throw new Error('Missing handler \\\\'' + 'updateApp' + \`\\\\' when evaluating template 'Parent'\`)
}
extra.handlers['click' + 2] = extra.handlers['click' + 2] || context['updateApp'].bind(owner);
p2.on['click'] = extra.handlers['click' + 2];
c2.push({text: \`Update App State\`});
var _3 = 'children';
let c4 = [], p4 = {key:4,attrs:{class: _3}};
var vn4 = h('div', p4, c4);
c1.push(vn4);
//WIDGET
let def6;
let w7 = 7 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[7]] : false;
let _5_index = c4.length;
c4.push(null);
let props7 = {val:context['state'].val};
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
def6 = w7.__owl__.renderPromise;
} else {
w7.destroy();
w7 = false;
}
}
if (!w7) {
let widgetKey7 = \`Child\`;
let W7 = context.widgets && context.widgets[widgetKey7] || QWeb.widgets[widgetKey7];
if (!W7) {throw new Error('Cannot find the definition of widget \\"' + widgetKey7 + '\\"')}
w7 = new W7(owner, props7);
context.__owl__.cmap[7] = w7.__owl__.id;
def6 = w7._prepare();
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: 7, hook: {insert(vn) {let nvn=w7._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c4[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
} else {
def6 = def6 || w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c4[_5_index]=pvnode;});
}
extra.promises.push(def6);
//WIDGET
let def9;
let w10 = 10 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[10]] : false;
let _8_index = c4.length;
const patchQueue10 = [];
c4.push(w10 && w10.__owl__.pvnode || null);
let props10 = {val:context['state'].val};
if (w10 && w10.__owl__.renderPromise && !w10.__owl__.vnode) {
if (utils.shallowEqual(props10, w10.__owl__.renderProps)) {
def9 = w10.__owl__.renderPromise;
} else {
w10.destroy();
w10 = false;
}
}
if (!w10) {
let widgetKey10 = \`AsyncChild\`;
let W10 = context.widgets && context.widgets[widgetKey10] || QWeb.widgets[widgetKey10];
if (!W10) {throw new Error('Cannot find the definition of widget \\"' + widgetKey10 + '\\"')}
w10 = new W10(owner, props10);
context.__owl__.cmap[10] = w10.__owl__.id;
def9 = w10._prepare();
def9 = def9.then(vnode=>{let pvnode=h(vnode.sel, {key: 10, hook: {insert(vn) {let nvn=w10._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w10.destroy();}}});c4[_8_index]=pvnode;w10.__owl__.pvnode = pvnode;});
} else {
def9 = def9 || w10._updateProps(props10, extra.forceUpdate, patchQueue10);
def9 = def9.then(()=>{if (w10.__owl__.isDestroyed) {return};let pvnode=w10.__owl__.pvnode;c4[_8_index]=pvnode;});
}
def9.then(w10._applyPatchQueue.bind(w10, patchQueue10));
return vn1;
}"
`;
exports[`async rendering fast t-widget with t-asyncroot directive 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('button', p2, c2);
c1.push(vn2);
if (!context['updateApp']) {
throw new Error('Missing handler \\\\'' + 'updateApp' + \`\\\\' when evaluating template 'Parent'\`)
}
extra.handlers['click' + 2] = extra.handlers['click' + 2] || context['updateApp'].bind(owner);
p2.on['click'] = extra.handlers['click' + 2];
c2.push({text: \`Update App State\`});
var _3 = 'children';
let c4 = [], p4 = {key:4,attrs:{class: _3}};
var vn4 = h('div', p4, c4);
c1.push(vn4);
//WIDGET
let def6;
let w7 = 7 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[7]] : false;
let _5_index = c4.length;
const patchQueue7 = [];
c4.push(w7 && w7.__owl__.pvnode || null);
let props7 = {val:context['state'].val};
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
def6 = w7.__owl__.renderPromise;
} else {
w7.destroy();
w7 = false;
}
}
if (!w7) {
let widgetKey7 = \`Child\`;
let W7 = context.widgets && context.widgets[widgetKey7] || QWeb.widgets[widgetKey7];
if (!W7) {throw new Error('Cannot find the definition of widget \\"' + widgetKey7 + '\\"')}
w7 = new W7(owner, props7);
context.__owl__.cmap[7] = w7.__owl__.id;
def6 = w7._prepare();
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: 7, hook: {insert(vn) {let nvn=w7._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c4[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
} else {
def6 = def6 || w7._updateProps(props7, extra.forceUpdate, patchQueue7);
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c4[_5_index]=pvnode;});
}
def6.then(w7._applyPatchQueue.bind(w7, patchQueue7));
//WIDGET
let def9;
let w10 = 10 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[10]] : false;
let _8_index = c4.length;
c4.push(null);
let props10 = {val:context['state'].val};
if (w10 && w10.__owl__.renderPromise && !w10.__owl__.vnode) {
if (utils.shallowEqual(props10, w10.__owl__.renderProps)) {
def9 = w10.__owl__.renderPromise;
} else {
w10.destroy();
w10 = false;
}
}
if (!w10) {
let widgetKey10 = \`AsyncChild\`;
let W10 = context.widgets && context.widgets[widgetKey10] || QWeb.widgets[widgetKey10];
if (!W10) {throw new Error('Cannot find the definition of widget \\"' + widgetKey10 + '\\"')}
w10 = new W10(owner, props10);
context.__owl__.cmap[10] = w10.__owl__.id;
def9 = w10._prepare();
def9 = def9.then(vnode=>{let pvnode=h(vnode.sel, {key: 10, hook: {insert(vn) {let nvn=w10._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w10.destroy();}}});c4[_8_index]=pvnode;w10.__owl__.pvnode = pvnode;});
} else {
def9 = def9 || w10._updateProps(props10, extra.forceUpdate, extra.patchQueue);
def9 = def9.then(()=>{if (w10.__owl__.isDestroyed) {return};let pvnode=w10.__owl__.pvnode;c4[_8_index]=pvnode;});
}
extra.promises.push(def9);
return vn1;
}"
`;
exports[`async rendering t-widget with t-asyncroot directive: mixed re-renderings 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('button', p2, c2);
c1.push(vn2);
if (!context['updateApp']) {
throw new Error('Missing handler \\\\'' + 'updateApp' + \`\\\\' when evaluating template 'Parent'\`)
}
extra.handlers['click' + 2] = extra.handlers['click' + 2] || context['updateApp'].bind(owner);
p2.on['click'] = extra.handlers['click' + 2];
c2.push({text: \`Update App State\`});
var _3 = 'children';
let c4 = [], p4 = {key:4,attrs:{class: _3}};
var vn4 = h('div', p4, c4);
c1.push(vn4);
//WIDGET
let def6;
let w7 = 7 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[7]] : false;
let _5_index = c4.length;
c4.push(null);
let props7 = {val:context['state'].val};
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
def6 = w7.__owl__.renderPromise;
} else {
w7.destroy();
w7 = false;
}
}
if (!w7) {
let widgetKey7 = \`Child\`;
let W7 = context.widgets && context.widgets[widgetKey7] || QWeb.widgets[widgetKey7];
if (!W7) {throw new Error('Cannot find the definition of widget \\"' + widgetKey7 + '\\"')}
w7 = new W7(owner, props7);
context.__owl__.cmap[7] = w7.__owl__.id;
def6 = w7._prepare();
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: 7, hook: {insert(vn) {let nvn=w7._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c4[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
} else {
def6 = def6 || w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c4[_5_index]=pvnode;});
}
extra.promises.push(def6);
//WIDGET
let def9;
let w10 = 10 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[10]] : false;
let _8_index = c4.length;
const patchQueue10 = [];
c4.push(w10 && w10.__owl__.pvnode || null);
let props10 = {val:context['state'].val};
if (w10 && w10.__owl__.renderPromise && !w10.__owl__.vnode) {
if (utils.shallowEqual(props10, w10.__owl__.renderProps)) {
def9 = w10.__owl__.renderPromise;
} else {
w10.destroy();
w10 = false;
}
}
if (!w10) {
let widgetKey10 = \`AsyncChild\`;
let W10 = context.widgets && context.widgets[widgetKey10] || QWeb.widgets[widgetKey10];
if (!W10) {throw new Error('Cannot find the definition of widget \\"' + widgetKey10 + '\\"')}
w10 = new W10(owner, props10);
context.__owl__.cmap[10] = w10.__owl__.id;
def9 = w10._prepare();
def9 = def9.then(vnode=>{let pvnode=h(vnode.sel, {key: 10, hook: {insert(vn) {let nvn=w10._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w10.destroy();}}});c4[_8_index]=pvnode;w10.__owl__.pvnode = pvnode;});
} else {
def9 = def9 || w10._updateProps(props10, extra.forceUpdate, patchQueue10);
def9 = def9.then(()=>{if (w10.__owl__.isDestroyed) {return};let pvnode=w10.__owl__.pvnode;c4[_8_index]=pvnode;});
}
def9.then(w10._applyPatchQueue.bind(w10, patchQueue10));
return vn1;
}"
`;
exports[`class and style attributes with t-widget dynamic t-att-style is properly added and updated on widget root el 1`] = `
"function anonymous(context,extra
) {
@@ -10,11 +253,11 @@ exports[`class and style attributes with t-widget dynamic t-att-style is properl
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
const _5 = context['state'].style;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -51,11 +294,11 @@ exports[`class and style attributes with t-widget t-att-class is properly added/
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
const _5 = {a:context['state'].a,b:context['state'].b};
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -112,10 +355,10 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
context.number_value = _4[i];
//WIDGET
let key8 = context['number'];
let _5_index = c1.length;
c1.push(null);
let def6;
let w7 = key8 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key8]] : false;
let _5_index = c1.length;
c1.push(null);
let props7 = {};
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
@@ -153,10 +396,10 @@ exports[`composition t-widget with dynamic value 1`] = `
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -193,10 +436,10 @@ exports[`composition t-widget with dynamic value 2 1`] = `
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -233,10 +476,10 @@ exports[`other directives with t-widget t-on with handler bound to argument 1`]
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -273,10 +516,10 @@ exports[`other directives with t-widget t-on with handler bound to empty object
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -313,10 +556,10 @@ exports[`other directives with t-widget t-on with handler bound to empty object
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -353,10 +596,10 @@ exports[`other directives with t-widget t-on with handler bound to object 1`] =
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -393,10 +636,10 @@ exports[`other directives with t-widget t-on with prevent and self modifiers (or
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -433,10 +676,10 @@ exports[`other directives with t-widget t-on with self and prevent modifiers (or
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -473,10 +716,10 @@ exports[`other directives with t-widget t-on with self modifier 1`] = `
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -513,10 +756,10 @@ exports[`other directives with t-widget t-on with stop and/or prevent modifiers
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -554,10 +797,10 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
var vn1 = h('div', p1, c1);
//WIDGET
let key5 = 'somestring';
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = key5 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key5]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {flag:context['state'].flag};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
@@ -584,6 +827,153 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
}"
`;
exports[`t-model directive .lazy modifier 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('input', p2, c2);
c1.push(vn2);
p2.props = {value: context.state['text']};
extra.handlers['change' + 2] = extra.handlers['change' + 2] || ((ev) => {context.state['text'] = ev.target.value});
p2.on['change'] = extra.handlers['change' + 2];
let c3 = [], p3 = {key:3};
var vn3 = h('span', p3, c3);
c1.push(vn3);
var _4 = context['state'].text;
if (_4 || _4 === 0) {
c3.push({text: _4});
}
return vn1;
}"
`;
exports[`t-model directive basic use, on an input 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('input', p2, c2);
c1.push(vn2);
p2.props = {value: context.state['text']};
extra.handlers['input' + 2] = extra.handlers['input' + 2] || ((ev) => {context.state['text'] = ev.target.value});
p2.on['input'] = extra.handlers['input' + 2];
let c3 = [], p3 = {key:3};
var vn3 = h('span', p3, c3);
c1.push(vn3);
var _4 = context['state'].text;
if (_4 || _4 === 0) {
c3.push({text: _4});
}
return vn1;
}"
`;
exports[`t-model directive on a select 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('select', p2, c2);
c1.push(vn2);
p2.props = {value: context.state['color']};
extra.handlers['change' + 2] = extra.handlers['change' + 2] || ((ev) => {context.state['color'] = ev.target.value});
p2.on['change'] = extra.handlers['change' + 2];
var _3 = '';
let c4 = [], p4 = {key:4,attrs:{value: _3}};
var vn4 = h('option', p4, c4);
c2.push(vn4);
c4.push({text: \`Please select one\`});
var _5 = 'red';
let c6 = [], p6 = {key:6,attrs:{value: _5}};
var vn6 = h('option', p6, c6);
c2.push(vn6);
c6.push({text: \`Red\`});
var _7 = 'blue';
let c8 = [], p8 = {key:8,attrs:{value: _7}};
var vn8 = h('option', p8, c8);
c2.push(vn8);
c8.push({text: \`Blue\`});
let c9 = [], p9 = {key:9};
var vn9 = h('span', p9, c9);
c1.push(vn9);
c9.push({text: \`Choice: \`});
var _10 = context['state'].color;
if (_10 || _10 === 0) {
c9.push({text: _10});
}
return vn1;
}"
`;
exports[`t-model directive on an input type=radio 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 'radio';
var _3 = 'one';
var _4 = 'One';
let c5 = [], p5 = {key:5,attrs:{type: _2,id: _3,value: _4},on:{}};
var vn5 = h('input', p5, c5);
c1.push(vn5);
p5.props = {checked:context.state['choice'] === 'One'};
extra.handlers['click' + 5] = extra.handlers['click' + 5] || ((ev) => {context.state['choice'] = ev.target.value});
p5.on['click'] = extra.handlers['click' + 5];
var _6 = 'radio';
var _7 = 'two';
var _8 = 'Two';
let c9 = [], p9 = {key:9,attrs:{type: _6,id: _7,value: _8},on:{}};
var vn9 = h('input', p9, c9);
c1.push(vn9);
p9.props = {checked:context.state['choice'] === 'Two'};
extra.handlers['click' + 9] = extra.handlers['click' + 9] || ((ev) => {context.state['choice'] = ev.target.value});
p9.on['click'] = extra.handlers['click' + 9];
let c10 = [], p10 = {key:10};
var vn10 = h('span', p10, c10);
c1.push(vn10);
c10.push({text: \`Choice: \`});
var _11 = context['state'].choice;
if (_11 || _11 === 0) {
c10.push({text: _11});
}
return vn1;
}"
`;
exports[`t-model directive on an input, type=checkbox 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 'checkbox';
let c3 = [], p3 = {key:3,attrs:{type: _2},on:{}};
var vn3 = h('input', p3, c3);
c1.push(vn3);
p3.props = {checked: context.state['flag']};
extra.handlers['input' + 3] = extra.handlers['input' + 3] || ((ev) => {context.state['flag'] = ev.target.checked});
p3.on['input'] = extra.handlers['input' + 3];
let c4 = [], p4 = {key:4};
var vn4 = h('span', p4, c4);
c1.push(vn4);
if (context['state'].flag) {
c4.push({text: \`yes\`});
}
else {
c4.push({text: \`no\`});
}
return vn1;
}"
`;
exports[`t-slot directive can define and call slots 1`] = `
"function anonymous(context,extra
) {
@@ -594,10 +984,10 @@ exports[`t-slot directive can define and call slots 1`] = `
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
+459 -1
View File
@@ -6,7 +6,8 @@ import {
makeTestEnv,
nextMicroTick,
nextTick,
normalize
normalize,
editInput
} from "./helpers";
//------------------------------------------------------------------------------
@@ -24,6 +25,7 @@ let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
env.qweb.addTemplate("Widget", "<div></div>");
env.qweb.addTemplate(
"Counter",
`<div><t t-esc="state.counter"/><button t-on-click="inc">Inc</button></div>`
@@ -82,6 +84,19 @@ describe("basic widget properties", () => {
expect(fixture.innerHTML).toBe("<div></div>");
});
test("crashes if it cannot find a template", async () => {
expect.assertions(1);
class SomeWidget extends Component<any, any, any> {}
const widget = new SomeWidget(env);
try {
await widget.mount(fixture);
} catch (e) {
expect(e.message).toBe(
'Could not find template for component "SomeWidget"'
);
}
});
test("can be clicked on and updated", async () => {
const counter = new Counter(env);
await counter.mount(fixture);
@@ -2352,6 +2367,194 @@ describe("async rendering", () => {
await nextTick();
expect(destroyCount).toBe(0);
});
test("delayed t-widget with t-asyncroot directive", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<t t-widget="Child" val="state.val"/>
<t t-widget="AsyncChild" t-asyncroot="1" val="state.val"/>
</div>
</div>
<span t-name="Child"><t t-esc="props.val"/></span>
</templates>
`);
let def;
class Parent extends Widget {
widgets = { Child, AsyncChild };
state = { val: 0 };
updateApp() {
this.state.val++;
}
}
class Child extends Widget {}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>0</span><span>0</span>"
);
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>1</span><span>0</span>"
);
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>1</span><span>1</span>"
);
});
test("fast t-widget with t-asyncroot directive", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<t t-widget="Child" t-asyncroot="1" val="state.val"/>
<t t-widget="AsyncChild" val="state.val"/>
</div>
</div>
<span t-name="Child"><t t-esc="props.val"/></span>
</templates>
`);
let def;
class Parent extends Widget {
widgets = { Child, AsyncChild };
state = { val: 0 };
updateApp() {
this.state.val++;
}
}
class Child extends Widget {}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>0</span><span>0</span>"
);
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>1</span><span>0</span>"
);
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>1</span><span>1</span>"
);
});
test("t-widget with t-asyncroot directive: mixed re-renderings", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<t t-widget="Child" val="state.val"/>
<t t-widget="AsyncChild" t-asyncroot="1" val="state.val"/>
</div>
</div>
<span t-name="Child" t-on-click="increment">
<t t-esc="state.val"/>/<t t-esc="props.val"/>
</span>
</templates>
`);
let def;
class Parent extends Widget {
widgets = { Child, AsyncChild };
state = { val: 0 };
updateApp() {
this.state.val++;
}
}
class Child extends Widget {
state = { val: 0 };
increment() {
this.state.val++;
}
}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>0/0</span><span>0/0</span>"
);
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>0/1</span><span>0/0</span>"
);
// click on each Child to increment their local counter
const children = parent.el!.querySelectorAll("span");
children[0]!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>1/1</span><span>0/0</span>"
);
children[1]!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>1/1</span><span>1/0</span>"
);
// finalize first re-rendering (coming from the props update)
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe(
"<span>1/1</span><span>1/1</span>"
);
});
});
describe("updating environment", () => {
@@ -2714,3 +2917,258 @@ describe("t-slot directive", () => {
);
});
});
describe("t-model directive", () => {
test("basic use, on an input", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<input t-model="text"/>
<span><t t-esc="state.text"/></span>
</div>
</templates>`);
class SomeComponent extends Widget {
state = { text: "" };
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
const input = fixture.querySelector("input")!;
await editInput(input, "test");
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
});
test("on an input, type=checkbox", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<input type="checkbox" t-model="flag"/>
<span>
<t t-if="state.flag">yes</t>
<t t-else="1">no</t>
</span>
</div>
</templates>`);
class SomeComponent extends Widget {
state = { flag: false };
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><input type="checkbox"><span>no</span></div>'
);
let input = fixture.querySelector("input")!;
input.click();
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><input type="checkbox"><span>yes</span></div>'
);
expect(comp.state.flag).toBe(true);
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
input.click();
await nextTick();
expect(comp.state.flag).toBe(false);
});
test("on an textarea", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<textarea t-model="text"/>
<span><t t-esc="state.text"/></span>
</div>
</templates>`);
class SomeComponent extends Widget {
state = { text: "" };
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><textarea></textarea><span></span></div>"
);
const textarea = fixture.querySelector("textarea")!;
await editInput(textarea, "test");
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe(
"<div><textarea></textarea><span>test</span></div>"
);
});
test("on an input type=radio", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<input type="radio" id="one" value="One" t-model="choice"/>
<input type="radio" id="two" value="Two" t-model="choice"/>
<span>Choice: <t t-esc="state.choice"/></span>
</div>
</templates>`);
class SomeComponent extends Widget {
state = { choice: "" };
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><input type="radio" id="one" value="One"><input type="radio" id="two" value="Two"><span>Choice: </span></div>'
);
const firstInput = fixture.querySelector("input")!;
firstInput.click();
await nextTick();
expect(comp.state.choice).toBe("One");
expect(fixture.innerHTML).toBe(
'<div><input type="radio" id="one" value="One"><input type="radio" id="two" value="Two"><span>Choice: One</span></div>'
);
const secondInput = fixture.querySelectorAll("input")[1];
secondInput.click();
await nextTick();
expect(comp.state.choice).toBe("Two");
expect(fixture.innerHTML).toBe(
'<div><input type="radio" id="one" value="One"><input type="radio" id="two" value="Two"><span>Choice: Two</span></div>'
);
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
});
test("on a select", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<select t-model="color">
<option value="">Please select one</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
<span>Choice: <t t-esc="state.color"/></span>
</div>
</templates>`);
class SomeComponent extends Widget {
state = { color: "" };
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><select><option value="">Please select one</option><option value="red">Red</option><option value="blue">Blue</option></select><span>Choice: </span></div>'
);
const select = fixture.querySelector("select")!;
select.value = "red";
select.dispatchEvent(new Event("change"));
await nextTick();
expect(comp.state.color).toBe("red");
expect(fixture.innerHTML).toBe(
'<div><select><option value="">Please select one</option><option value="red">Red</option><option value="blue">Blue</option></select><span>Choice: red</span></div>'
);
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
});
test(".lazy modifier", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<input t-model.lazy="text"/>
<span><t t-esc="state.text"/></span>
</div>
</templates>`);
class SomeComponent extends Widget {
state = { text: "" };
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
const input = fixture.querySelector("input")!;
input.value = "test";
input.dispatchEvent(new Event("input"));
await nextTick();
expect(comp.state.text).toBe("");
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
input.dispatchEvent(new Event("change"));
await nextTick();
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
});
test(".trim modifier", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<input t-model.trim="text"/>
<span><t t-esc="state.text"/></span>
</div>
</templates>`);
class SomeComponent extends Widget {
state = { text: "" };
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
const input = fixture.querySelector("input")!;
await editInput(input, " test ");
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
});
test(".number modifier", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<input t-model.number="number"/>
<span><t t-esc="state.number"/></span>
</div>
</templates>`);
class SomeComponent extends Widget {
state = { number: 0 };
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input><span>0</span></div>");
const input = fixture.querySelector("input")!;
await editInput(input, "13");
expect(comp.state.number).toBe(13);
expect(fixture.innerHTML).toBe("<div><input><span>13</span></div>");
await editInput(input, "invalid");
expect(comp.state.number).toBe("invalid");
expect(fixture.innerHTML).toBe("<div><input><span>invalid</span></div>");
});
test("content is the default slot", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<t t-widget="Dialog">
<span>sts rocks</span>
</t>
</div>
<div t-name="Dialog"><t t-slot="default"/></div>
</templates>
`);
class Dialog extends Widget {}
class Parent extends Widget {
widgets = { Dialog };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div><span>sts rocks</span></div></div>"
);
});
});
+10
View File
@@ -91,3 +91,13 @@ export function patchNextFrame(f: Function) {
export function unpatchNextFrame() {
UTILS.nextFrame = nextFrame;
}
export async function editInput(
input: HTMLInputElement | HTMLTextAreaElement,
value: string
) {
input.value = value;
input.dispatchEvent(new Event("input"));
input.dispatchEvent(new Event("change"));
return nextTick();
}
+1 -1
View File
@@ -1105,7 +1105,7 @@ describe("loading templates", () => {
test("does not crash if string does not have templates", () => {
const data = "";
qweb.addTemplates(data);
expect(Object.keys(qweb.templates)).toEqual(["default"]);
expect(Object.keys(qweb.templates)).toEqual([]);
});
});
+139
View File
@@ -182,6 +182,145 @@ describe("basic use", () => {
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
});
test("getters are memoized", async () => {
const state = {
beers: {
1: {
id: 1,
name: "bertinchamps",
tasterID: 1
}
},
tasters: {
1: {
id: 1,
name: "aaron"
}
}
};
let n = 0;
const getters = {
beerTasterName({ state }, beerID) {
n++;
return state.tasters[state.beers[beerID].tasterID].name;
},
bestBeerName({ state }) {
n++;
return state.beers[1].name;
},
};
const store = new Store({ state, mutations: {}, actions: {}, getters });
expect((<any>store.getters).bestBeerName()).toBe("bertinchamps");
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
expect(n).toBe(2);
expect((<any>store.getters).bestBeerName()).toBe("bertinchamps");
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
expect(n).toBe(2);
});
test("getters taking Array as argument aren't memoized", async () => {
const state = {
beers: {
1: {
id: 1,
name: "bertinchamps",
tasterID: 1
}
},
};
let n = 0;
const getters = {
getBeerNames({ state }, beerIDs) {
n++;
return beerIDs.map(beerID => {
return state.beers[beerID].name;
});
}
};
const store = new Store({ state, mutations: {}, actions: {}, getters });
expect((<any>store.getters).getBeerNames([1])).toEqual(["bertinchamps"]);
expect(n).toBe(1);
expect((<any>store.getters).getBeerNames([1])).toEqual(["bertinchamps"]);
expect(n).toBe(2);
});
test("getters cache is nuked on store changes", async () => {
const state = {
beers: {
1: {
id: 1,
name: "bertinchamps",
tasterID: 1
}
},
tasters: {
1: {
id: 1,
name: "aaron"
},
2: {
id: 2,
name: "gery"
}
}
};
const mutations = {
changeTaster({ state }, {beerID, tasterID}) {
state.beers[beerID].tasterID = tasterID;
}
};
let n = 0;
const getters = {
beerTasterName({ state }, beerID) {
n++;
return state.tasters[state.beers[beerID].tasterID].name;
},
};
const store = new Store({ state, mutations: mutations, actions: {}, getters });
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
expect(n).toBe(1);
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
expect(n).toBe(1);
store.commit('changeTaster', {beerID: 1, tasterID: 2});
await nextTick();
expect((<any>store.getters).beerTasterName(1)).toBe("gery");
expect(n).toBe(2);
});
test("getters cache is disabled during a mutation", async () => {
const state = {
beers: {
1: {
id: 1,
name: "bertinchamps"
},
}
};
const mutations = {
renameBeer({ state, getters }, beerID) {
expect(getters.beerName(beerID)).toBe('bertinchamps');
state.beers[1].name = 'chouffe';
expect(getters.beerName(beerID)).toBe('chouffe');
}
};
let n = 0;
const getters = {
beerName({ state }, beerID) {
n++;
return state.beers[beerID].name;
},
};
const store = new Store({ state, mutations: mutations, actions: {}, getters });
store.commit('renameBeer', 1);
expect((<any>store.getters).beerName(1)).toBe('chouffe');
await nextTick();
expect(n).toBe(3);
});
test("getters given to actions", async () => {
expect.assertions(3);
const state = {
+170
View File
@@ -0,0 +1,170 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(
`Benchmarking Owl v${owl.__info__.version} (build date: ${
owl.__info__.date
})`
);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.14.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+3 -9
View File
@@ -128,7 +128,9 @@ class App extends owl.Component {
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
const index = this.state.messages.findIndex(
m => m.id === event.detail.id
);
this.state.messages.splice(index, 1);
});
}
@@ -146,14 +148,6 @@ class App extends owl.Component {
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
+2 -2
View File
@@ -12,11 +12,11 @@
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<input type="checkbox" id="multipleflag" t-model="multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<input type="checkbox" id="clearFlag" t-model="clearAfterFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
+1
View File
@@ -217,6 +217,7 @@ class Main extends React.Component {
"Number of messages: ",
this.state.messages.length
),
React.createElement("hr"),
React.createElement(
"div",
{ className: "title" },
+1
View File
@@ -34,6 +34,7 @@
<li><a href="benchmarks/owl-0.11.0">OWL 0.11.0</a></li>
<li><a href="benchmarks/owl-0.12.0">OWL 0.12.0</a></li>
<li><a href="benchmarks/owl-0.13.0">OWL 0.13.0</a></li>
<li><a href="benchmarks/owl-0.14.0">OWL 0.14.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul>
<ul>
+312 -192
View File
@@ -1,33 +1,6 @@
const CLICK_COUNTER = `class ClickCounter extends owl.Component {
constructor() {
super(...arguments);
this.state = { value: 0 };
}
const COMPONENTS = `// In this example, we show how components can be defined and created.
increment() {
this.state.value++;
}
}
const qweb = new owl.QWeb(TEMPLATES);
const counter = new ClickCounter({ qweb });
counter.mount(document.body);
`;
const CLICK_COUNTER_XML = `<templates>
<button t-name="ClickCounter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
</templates>`;
const CLICK_COUNTER_CSS = `button {
color: darkred;
font-size: 30px;
width: 220px;
}`;
const CLICK_COUNTER_ESNEXT = `// This example will not work if your browser does not support ESNext class fields
class ClickCounter extends owl.Component {
class Counter extends owl.Component {
state = { value: 0 };
increment() {
@@ -35,68 +8,39 @@ class ClickCounter extends owl.Component {
}
}
const qweb = new owl.QWeb(TEMPLATES);
const counter = new ClickCounter({ qweb });
counter.mount(document.body);
`;
const WIDGET_COMPOSITION = `// This example will not work if your browser does not support ESNext class fields
class ClickCounter extends owl.Component {
state = { value: 0 };
increment() {
this.state.value++;
}
}
class InputWidget extends owl.Component {
state = {text: ""};
updateVal(event) {
let value = event.target.value;
if (this.props.reverse) {
value = value.split("").reverse().join("");
}
this.state.text = value
}
}
// Main root widget
class App extends owl.Component {
widgets = {ClickCounter, InputWidget};
widgets = { Counter };
}
// Application setup
// Note that the xml templates are injected into the global TEMPLATES variable.
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
app.mount(document.body);
`;
const WIDGET_COMPOSITION_XML = `<templates>
<button t-name="ClickCounter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
<div t-name="InputWidget">
<input t-on-input="updateVal" placeholder="type here some text..."/>
<span><t t-esc="state.text"/></span>
</div>
const COMPONENTS_XML = `<templates>
<button t-name="Counter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
<div t-name="App">
<t t-widget="ClickCounter"/>
<t t-widget="InputWidget" reverse="true"/>
<t t-widget="Counter"/>
<t t-widget="Counter"/>
</div>
</templates>`;
const WIDGET_COMPOSITION_CSS = `button, input {
const COMPONENTS_CSS = `button {
font-size: 20px;
width: 220px;
margin: 5px;
}`;
const ANIMATION = `// This example will not work if your browser does not support ESNext class fields
class ClickCounter extends owl.Component {
const ANIMATION = `// The goal of this component is to see how the t-transition directive can be
// used to generate simple transition effects.
class Counter extends owl.Component {
state = { value: 0 };
increment() {
@@ -105,15 +49,11 @@ class ClickCounter extends owl.Component {
}
class App extends owl.Component {
state = {flag: false, widgetFlag: false, numbers: []};
widgets = {ClickCounter};
state = { flag: false, widgetFlag: false, numbers: [] };
widgets = { Counter };
toggle() {
this.state.flag = !this.state.flag;
}
toggleWidget() {
this.state.widgetFlag = !this.state.widgetFlag;
toggle(key) {
this.state[key] = !this.state[key];
}
addNumber() {
@@ -129,46 +69,44 @@ app.mount(document.body);
`;
const ANIMATION_XML = `<templates>
<button t-name="ClickCounter" t-on-click="increment" class="clickcounter">
<button t-name="Counter" t-on-click="increment" class="clickcounter">
Click Me! [<t t-esc="state.value"/>]
</button>
<div t-name="App">
<h2>Transition on DOM element</h2>
<h2>Transition on DOM element</h2>
<div class="demo">
<button t-on-click="toggle">Toggle square</button>
<div>
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
<div class="demo">
<button t-on-click="toggle('flag')">Toggle square</button>
<div>
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
</div>
</div>
</div>
<h2>Transition on sub widgets</h2>
<h2>Transition on sub widgets</h2>
<div class="demo">
<button t-on-click="toggleWidget">
Toggle widget
</button>
<div>
<t t-widget="ClickCounter" t-if="state.widgetFlag" t-transition="fade"/>
<div class="demo">
<button t-on-click="toggle('widgetFlag')">Toggle widget</button>
<div>
<t t-widget="Counter" t-if="state.widgetFlag" t-transition="fade"/>
</div>
</div>
</div>
<h2>Transition on lists</h2>
<p>Transitions can also be applied on lists</p>
<div class="demo">
<button t-on-click="addNumber">Add a number</button>
<div>
<t t-foreach="state.numbers" t-as="n">
<span t-transition="fade" class="numberspan"><t t-esc="n"/></span>
</t>
<h2>Transition on lists</h2>
<p>Transitions can also be applied on lists</p>
<div class="demo">
<button t-on-click="addNumber">Add a number</button>
<div>
<t t-foreach="state.numbers" t-as="n">
<span t-transition="fade" class="numberspan"><t t-esc="n"/></span>
</t>
</div>
</div>
</div>
<h2>Simple CSS animation</h2>
<p>Remember, normal CSS still apply: for example, a simple flash animation with pure css </p>
<div><a class="btn flash">Click</a></div>
<h2>Simple CSS animation</h2>
<p>Remember, normal CSS still apply: for example, a simple flash animation with pure css </p>
<div><a class="btn flash">Click</a>
</div>
</div>
</templates>
`;
@@ -177,6 +115,7 @@ const ANIMATION_CSS = `button {
font-size: 18px;
height: 35px;
}
.btn {
cursor: pointer;
padding: 5px;
@@ -184,7 +123,6 @@ const ANIMATION_CSS = `button {
background-color: #dddddd;
}
.flash {
background-position: center;
transition: background .6s;
@@ -195,7 +133,6 @@ const ANIMATION_CSS = `button {
transition: background 0s;
}
.square {
background-color: red;
width: 100px;
@@ -210,6 +147,7 @@ const ANIMATION_CSS = `button {
.fade-enter-active, .fade-leave-active {
transition: opacity .6s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
@@ -233,7 +171,14 @@ const ANIMATION_CSS = `button {
}
`;
const LIFECYCLE_DEMO = `class HookWidget extends owl.Component {
const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks
//
// The root widget controls a sub widget (DemoWidget). It logs all its lifecycle
// methods in the console. Try modifying its state by clicking on it, or by
// clicking on the two main buttons, and look into the console to see what
// happens.
class DemoWidget extends owl.Component {
constructor() {
super(...arguments);
this.state = { n: 0 };
@@ -262,37 +207,60 @@ const LIFECYCLE_DEMO = `class HookWidget extends owl.Component {
}
}
class ParentWidget extends owl.Component {
constructor() {
super(...arguments);
this.widgets = { HookWidget };
this.state = { n: 0, flag: true };
}
class App extends owl.Component {
widgets = { DemoWidget };
state = { n: 0, flag: true };
increment() {
this.state.n++;
}
toggleSubWidget() {
this.state.flag = !this.state.flag;
}
}
const qweb = new owl.QWeb(TEMPLATES);
const widget = new ParentWidget({ qweb });
widget.mount(document.body);
const app = new App({ qweb });
app.mount(document.body);
`;
const LIFECYCLE_DEMO_XML = `<templates>
<div t-name="ParentWidget">
<button t-on-click="increment">Increment</button>
<button t-on-click="toggleSubWidget">ToggleSubWidget</button>
<div t-if="state.flag">
<t t-widget="HookWidget" n="state.n"/>
</div>
<div t-name="DemoWidget" t-on-click="increment" class="demo">
<div>Demo Sub Widget</div>
<div>(click on me to update me)</div>
<div>Props: <t t-esc="props.n"/>, State: <t t-esc="state.n"/>. </div>
</div>
<div t-name="App">
<button t-on-click="increment">Increment Parent State</button>
<button t-on-click="toggleSubWidget">Toggle SubWidget</button>
<div t-if="state.flag">
<t t-widget="DemoWidget" n="state.n"/>
</div>
<div t-name="HookWidget" t-on-click="increment">Demo Sub Widget. Props: <t t-esc="props.n"/>. State: <t t-esc="state.n"/>. (click on me to update me)</div>
</div>
</templates>`;
const TODO_APP_STORE = `const ENTER_KEY = 13;
const LIFECYCLE_CSS = `button {
font-size: 18px;
margin: 5px;
}
.demo {
margin: 10px;
padding: 10px;
background-color: #dddddd;
width: 250px;
}`;
const TODO_APP_STORE = `// This example is an implementation of the TodoList application, from the
// www.todomvc.com project. This is a non trivial application with some
// interesting user interactions. It uses the local storage for persistence.
//
// In this implementation, we use the owl Store class to manage the state. It
// is very similar to the VueX store.
const ENTER_KEY = 13;
const ESC_KEY = 27;
const LOCALSTORAGE_KEY = "todos-odoo";
@@ -937,7 +905,20 @@ html .clear-completed:active {
}
`;
const RESPONSIVE = `class Navbar extends owl.Component {}
const RESPONSIVE = `// In this example, we show how we can modify keys in the global environment to
// make a responsive application.
//
// The main idea is to have a "isMobile" key in the environment, then listen
// to resize events and update the env if needed. Then, the whole interface
// will be updated, creating and destroying widgets as needed.
//
// To see this in action, try resizing the window. The application will switch
// to mobile mode whenever it has less than 768px.
//------------------------------------------------------------------------------
// Components
//------------------------------------------------------------------------------
class Navbar extends owl.Component {}
class ControlPanel extends owl.Component {
widgets = { MobileSearchView };
@@ -958,6 +939,9 @@ class App extends owl.Component {
widgets = { Navbar, ControlPanel, FormView, Chatter };
}
//------------------------------------------------------------------------------
// Application Startup
//------------------------------------------------------------------------------
function isMobile() {
return window.innerWidth <= 768;
}
@@ -967,6 +951,7 @@ const env = {
isMobile: isMobile()
};
const app = new App(env);
app.mount(document.body);
@@ -983,44 +968,44 @@ window.addEventListener("resize", owl.utils.debounce(updateEnv, 20));
`;
const RESPONSIVE_XML = `<templates>
<div t-name="Navbar" class="navbar">Navbar</div>
<div t-name="Navbar" class="navbar">Navbar</div>
<div t-name="ControlPanel" class="controlpanel">
<h2>controlpanel</h2>
<t t-if="env.isMobile" t-widget="MobileSearchView"/>
</div>
<div t-name="FormView" class="formview">
<h2>formview</h2>
<t t-if="!env.isMobile" t-widget="AdvancedWidget"/>
</div>
<div t-name="Chatter" class="chatter">
<h2>Chatter</h2>
<t t-foreach="100" t-as="item"><div>Message <t t-esc="item"/></div></t>
</div>
<div t-name="MobileSearchView">MOBILE searchview</div>
<div t-name="App" class="app" t-att-class="{mobile: env.isMobile, desktop: !env.isMobile}">
<t t-widget="Navbar"/>
<t t-widget="ControlPanel"/>
<div class="content-wrapper" t-if="!env.isMobile">
<div class="content">
<t t-widget="FormView"/>
<t t-widget="Chatter"/>
</div>
</div>
<t t-else="1">
<t t-widget="FormView"/>
<t t-widget="Chatter"/>
</t>
</div>
<div t-name="AdvancedWidget">
This widget is only created in desktop mode.
<button>Button!</button>
<div t-name="ControlPanel" class="controlpanel">
<h2>Control Panel</h2>
<t t-if="env.isMobile" t-widget="MobileSearchView"/>
</div>
<div t-name="FormView" class="formview">
<h2>Form View</h2>
<t t-if="!env.isMobile" t-widget="AdvancedWidget"/>
</div>
<div t-name="Chatter" class="chatter">
<h2>Chatter</h2>
<t t-foreach="100" t-as="item"><div>Message <t t-esc="item"/></div></t>
</div>
<div t-name="MobileSearchView">Mobile searchview</div>
<div t-name="AdvancedWidget">
This widget is only created in desktop mode.
<button>Button!</button>
</div>
<div t-name="App" class="app" t-att-class="{mobile: env.isMobile, desktop: !env.isMobile}">
<t t-widget="Navbar"/>
<t t-widget="ControlPanel"/>
<div class="content-wrapper" t-if="!env.isMobile">
<div class="content">
<t t-widget="FormView"/>
<t t-widget="Chatter"/>
</div>
</div>
<t t-else="1">
<t t-widget="FormView"/>
<t t-widget="Chatter"/>
</t>
</div>
</templates>
`;
@@ -1036,6 +1021,7 @@ const RESPONSIVE_CSS = `body {
.app.desktop {
display: flex;
}
.navbar {
flex: 0 0 30px;
height: 30px;
@@ -1081,17 +1067,19 @@ const RESPONSIVE_CSS = `body {
}
`;
const SLOTS = `// This example will not work if your browser does not support ESNext class fields
// We show here how slots can be used to create generic components. In this
// example, the Card component is basically only a container, and is created
// by giving it slots, inside the t-widget directive.
const SLOTS = `// We show here how slots can be used to create generic components.
// In this example, the Card component is basically only a container. It is not
// aware of its content. It just knows where it should be (with t-slot).
// The parent widget define the content with t-set.
//
// Note that the t-on-click event, defined in the App template, is executed in
// the context of the App component, even though it is inside the Card component
class Card extends owl.Component {
state = { fullDisplay: true };
state = { showContent: true };
toggleDisplay() {
this.state.fullDisplay = !this.state.fullDisplay;
this.state.showContent = !this.state.showContent;
}
}
@@ -1119,11 +1107,11 @@ const app = new App({ qweb });
app.mount(document.body);`;
const SLOTS_XML = `<templates>
<div t-name="Card" class="card" t-att-class="state.fullDisplay ? 'full' : 'small'">
<div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'">
<div class="card-title">
<t t-esc="props.title"/><button t-on-click="toggleDisplay">Toggle</button>
</div>
<t t-if="state.fullDisplay">
<t t-if="state.showContent">
<div class="card-content" >
<t t-slot="content"/>
</div>
@@ -1196,32 +1184,161 @@ const SLOTS_CSS = `.main {
border-top: 1px solid white;
}`;
const EMPTY = `class App extends owl.Component {
const ASYNC_COMPONENTS = `// This example will not work if your browser does not support ESNext class fields
// In this example, we have 2 sub widgets, one of them being async (slow).
// However, we don't want renderings of the other sub widget to be delayed
// because of the slow widget. We use the 't-asyncroot' directive for this
// purpose. Try removing it to see the difference.
class App extends owl.Component {
widgets = {SlowWidget, NotificationList};
state = { value: 0, notifs: [] };
increment() {
this.state.value++;
const notif = "Value will be set to " + this.state.value;
this.state.notifs.push(notif);
setTimeout(() => {
var index = this.state.notifs.indexOf(notif);
this.state.notifs.splice(index, 1);
}, 3000);
}
}
class SlowWidget extends owl.Component {
willUpdateProps() {
// simulate a widget that needs to perform async stuff (e.g. an RPC)
// with the updated props before re-rendering itself
return new Promise(resolve => setTimeout(resolve, 1500));
}
}
class NotificationList extends owl.Component {}
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({qweb});
const app = new App({ qweb });
app.mount(document.body);
`;
const ASYNC_COMPONENTS_XML = `<templates>
<div t-name="App" class="app">
<button t-on-click="increment">Increment</button>
<t t-widget="SlowWidget" value="state.value"/>
<t t-widget="NotificationList" t-asyncroot="1" notifications="state.notifs"/>
</div>
<div t-name="SlowWidget" class="value" >
Current value: <t t-esc="props.value"/>
</div>
<div t-name="NotificationList" class="notification-list">
<t t-foreach="props.notifications" t-as="notif">
<div class="notification"><t t-esc="notif"/></div>
</t>
</div>
</templates>`;
const ASYNC_COMPONENTS_CSS = `.app {
width: 70%;
}
button {
color: darkred;
font-size: 30px;
width: 220px;
}
.value {
font-size: 26px;
padding: 20px;
}
.notification-list {
position: absolute;
top: 0;
right: 0;
}
.notification {
width: 150px;
margin: 4px 8px;
padding: 16px;
border: 1px solid: black;
background-color: lightgray;
}`;
const FORM = `// This example illustrate how the t-model directive can be used to synchronize
// data between html inputs (and select/textareas) and the state of a component.
// Note that there are two controls with t-model="color": they are totally
// synchronized.
class Form extends owl.Component {
state = {
text: "",
othertext: "",
number: 11,
color: "",
bool: false
};
}
// Application setup
const qweb = new owl.QWeb(TEMPLATES);
const form = new Form({ qweb });
form.mount(document.body);
`;
const FORM_XML = `<templates>
<div t-name="Form">
<h1>Form</h1>
<div>
Text (immediate): <input t-model="text"/>
</div>
<div>
Other text (lazy): <input t-model.lazy="othertext"/>
</div>
<div>
Number: <input t-model.number="number"/>
</div>
<div>
Boolean: <input type="checkbox" t-model="bool"/>
</div>
<div>
Color, with a select:
<select t-model="color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
<div>
Color, with radio buttons:
<span><input type="radio" name="color" id="red" value="red" t-model="color"/><label for="red">Red</label></span>
<span><input type="radio" name="color" id="blue" value="blue" t-model="color"/><label for="blue">Blue</label></span>
</div>
<hr/>
<h1>State</h1>
<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>Color: <t t-esc="state.color"/></div>
</div>
</templates>
`;
export const SAMPLES = [
{
description: "Click Counter",
code: CLICK_COUNTER,
xml: CLICK_COUNTER_XML,
css: CLICK_COUNTER_CSS
description: "Components",
code: COMPONENTS,
xml: COMPONENTS_XML,
css: COMPONENTS_CSS
},
{
description: "Click Counter (ESNext)",
code: CLICK_COUNTER_ESNEXT,
xml: CLICK_COUNTER_XML,
css: CLICK_COUNTER_CSS
},
{
description: "Widget Composition",
code: WIDGET_COMPOSITION,
xml: WIDGET_COMPOSITION_XML,
css: WIDGET_COMPOSITION_CSS
description: "Form Input Bindings",
code: FORM,
xml: FORM_XML
},
{
description: "Animations",
@@ -1232,7 +1349,8 @@ export const SAMPLES = [
{
description: "Lifecycle demo",
code: LIFECYCLE_DEMO,
xml: LIFECYCLE_DEMO_XML
xml: LIFECYCLE_DEMO_XML,
css: LIFECYCLE_CSS,
},
{
description: "Todo List App (with store)",
@@ -1247,13 +1365,15 @@ export const SAMPLES = [
xml: RESPONSIVE_XML
},
{
description: "Slots",
description: "Slots And Generic Components",
code: SLOTS,
xml: SLOTS_XML,
css: SLOTS_CSS
},
{
description: "Empty",
code: EMPTY
description: "Asynchronous components",
code: ASYNC_COMPONENTS,
xml: ASYNC_COMPONENTS_XML,
css: ASYNC_COMPONENTS_CSS
}
];
+1 -1
View File
@@ -2,7 +2,7 @@
<div t-name="TabbedEditor" class="tabbed-editor">
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
<t t-foreach="['js', 'xml', 'css']" t-as="tab">
<a t-ref="{{tab}}" t-if="props[tab]" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="setTab(tab)">
<a t-ref="{{tab}}" t-if="props[tab] !== false" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="setTab(tab)">
<t t-esc="tab"/>
</a>
</t>