Compare commits

..

32 Commits

Author SHA1 Message Date
Géry Debongnie 9e65b0686f [REL] bump to v0.23.0 2019-10-16 22:31:04 +02:00
Géry Debongnie 1d72164015 [FIX] component: support component reduced to a slot 2019-10-16 21:48:49 +02:00
Géry Debongnie 2825a5a55d [FIX] qweb: properly handle xml comments 2019-10-15 15:47:50 +02:00
Géry Debongnie d40ff53f5a [DOC] doc: add more information in store section 2019-10-14 15:00:31 +02:00
Aaron Bohy 23246c42aa [REF] *: snakecase filenames 2019-10-14 11:06:59 +02:00
Géry Debongnie 895fe7c60f [REF] store: replace ConnectedComponent by useStore hook
Closes #304
2019-10-14 11:06:59 +02:00
Géry Debongnie 19b10f7c1d [IMP] playground: update todoapp sample 2019-10-14 11:06:59 +02:00
Géry Debongnie 0788d9fe75 [REF] qweb: rename Context into CompilationContext 2019-10-14 11:06:59 +02:00
Géry Debongnie 4a346d0615 [FIX] qweb: properly remove t-ref reference
when the node is removed from the dom
2019-10-14 11:06:59 +02:00
Géry Debongnie 1337aa0107 [IMP] component: add current key for easier hook creation
closes #319
2019-10-14 10:25:34 +02:00
Géry Debongnie 3e8f60cefa [FIX] qweb: properly handle t-raw in various situations
closes #325
closes #327
2019-10-14 09:59:26 +02:00
Géry Debongnie 41b0618c93 [FIX] qweb: improve generated compiled code 2019-10-11 14:10:44 +02:00
Aaron Bohy 0928c189f4 [FIX] qweb: allow to combine t-esc with other directives
With this rev., when a t-esc is not set on a <t> tag, we create
it inside the node on which it is set, to ensure that the t-esc
directive is always set on <t> tags.

Same applies for t-raw.

Closes #324
2019-10-11 14:10:44 +02:00
Aaron Bohy 2f27768be2 [FIX] qweb: event modifier with arg in t-foreach
Closes #266
2019-10-11 14:01:07 +02:00
Aaron Bohy aa1b0f502d [FIX] tests: fix import 2019-10-11 14:01:07 +02:00
Géry Debongnie da1824f4dd [DOC] doc: improve navigation to main doc page
also, fixes a small error
2019-10-10 12:28:36 +02:00
Géry Debongnie 07dac970f7 [IMP[ component/hooks: add onWillStart and onWillUpdateProps 2019-10-09 15:09:26 +02:00
Géry Debongnie 5f6f081ae2 [IMP] playground: save changes to local storage
closes #316
closes #101
2019-10-09 10:15:09 +02:00
Géry Debongnie 1c3c2e5c51 [IMP] hooks/Context: add Context and useContext
Closes #310
2019-10-08 15:56:46 +02:00
Aaron Bohy 17ce73e6a9 [REF] hooks: improve makeLifecycleHook 2019-10-08 15:52:44 +02:00
Géry Debongnie cedb1f411f [REF] component: move template check in constructor 2019-10-08 13:51:55 +02:00
Géry Debongnie e7516be95c [DOC] component: remove doc for removed updateEnv method 2019-10-07 16:07:56 +02:00
Géry Debongnie 683993dbc4 [FIX] component/hooks: onWillUnmount was not properly called 2019-10-07 16:01:28 +02:00
Géry Debongnie 50d569411c [DOC] hooks: fixing broken link 2019-10-07 15:51:45 +02:00
Géry Debongnie c3453d35b9 [IMP] hooks/component: remove updateEnv, add useSubEnv
closes #182
2019-10-04 22:54:27 +02:00
Géry Debongnie b0dd0252a4 [REF] component: move method at proper place 2019-10-04 22:54:27 +02:00
Géry Debongnie e2b2e54e29 [DOC] tooling: add missing link 2019-10-04 22:12:56 +02:00
Géry Debongnie 766c378ac9 [DOC] hooks: add missing doc for onWillPatch/onPatched 2019-10-03 11:57:58 +02:00
Géry Debongnie 022b29b6a0 [IMP] component/qweb: remove t-mounted directive
The t-mounted directive main goals can be achieved with hooks, in a
better and more intuitive way.

closes #308
2019-10-03 10:57:24 +02:00
Géry Debongnie 367725d194 [IMP] hooks: add onWillPatch and onPatched
This commit also changes the behaviour of willPatch and patched: they no
longer transfer data from one to the other, because it can be done
cleanly by hooks and a closure.

closes #307
2019-10-03 10:20:28 +02:00
Géry Debongnie e82ed7dc8c [DOC] component: add information on svg components 2019-10-02 16:40:13 +02:00
Géry Debongnie 63c3d4f6b0 [IMP] playground: slightly improve default text 2019-10-01 21:37:36 +02:00
53 changed files with 3103 additions and 2292 deletions
+7 -3
View File
@@ -87,7 +87,11 @@ find some more information [here](doc/comparison.md).
## Documentation
The complete documentation can be found [here](doc/readme.md). The most important sections are:
A complete documentation for Owl can be found here:
- [Main documentation page](doc/readme.md).
The most important sections are:
- [Quick Start](doc/quick_start.md)
- [Component](doc/component.md)
@@ -100,8 +104,8 @@ Submit a PR!
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-0.22.0.js](https://github.com/odoo/owl/releases/download/v0.22.0/owl.js)
- [owl-0.22.0.min.js](https://github.com/odoo/owl/releases/download/v0.22.0/owl.min.js)
- [owl-0.23.0.js](https://github.com/odoo/owl/releases/download/v0.23.0/owl.js)
- [owl-0.23.0.min.js](https://github.com/odoo/owl/releases/download/v0.23.0/owl.min.js)
Some npm scripts are available:
+55 -29
View File
@@ -16,7 +16,6 @@
- [Event Handling](#event-handling)
- [Form Input Bindings](#form-input-bindings)
- [`t-key` Directive](#t-key-directive)
- [`t-mounted` Directive](#t-mounted-directive)
- [Semantics](#semantics)
- [Props Validation](#props-validation)
- [References](#references)
@@ -24,6 +23,7 @@
- [Asynchronous Rendering](#asynchronous-rendering)
- [Error Handling](#error-handling)
- [Functional Components](#functional-components)
- [SVG components](#svg-components)
## Overview
@@ -162,6 +162,11 @@ constructor.
}
```
There is another static property defined on the `Component` class: `current`.
This property is set to the currently being defined component (in the constructor).
This is the way [hooks](hooks.md) are able to get a reference to the target
component.
### Methods
We explain here all the public methods of the `Component` class.
@@ -195,11 +200,6 @@ We explain here all the public methods of the `Component` class.
always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
can be useful if we are handling large number of components.
- **`updateEnv(nextEnv)`**: update the environment of a component and all its
children. This forces a complete rerender. For example, this could be useful
if we have a `isMobile` key in the environment, to decide if we want a mobile
interface or a destkop one.
- **`destroy()`**. As its name suggests, this method will remove the component,
and perform all necessary cleanup, such as unmounting the component, its children,
removing the parent/children relationship. This method should almost never be
@@ -328,9 +328,6 @@ before an actual DOM patch, and is only intended to be used to save some local
DOM state. Also, it will not be called if the component is not in the DOM (this can
happen with components with `t-keepalive`).
The return value of this method will be given as the first argument of the
corresponding `patched` call.
#### `patched(snapshot)`
This hook is called whenever a component did actually update its DOM (most
@@ -341,8 +338,6 @@ with the DOM (for example, through an external library) whenever the
component was patched. Note that this hook will not be called if the compoent is
not in the DOM (this can happen with components with `t-keepalive`).
The `snapshot` parameter is the result of the previous `willPatch` call.
Updating the compoent state in this hook is possible, but not encouraged.
One need to be careful, because updates here will cause rerender, which in
turn will cause other calls to patched. So, we need to be particularly
@@ -760,24 +755,6 @@ 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
The `t-mounted` directive allows to register a callback to execute whenever the node
is inserted into the DOM.
```xml
<div><input t-ref="someInput" t-mounted="focusMe"/></div>
```
```js
class MyComponent extends owl.Component {
...
focusMe() {
this.refs.someInput.focus();
}
}
```
### Semantics
We give here an informal description of the way components are created/updated
@@ -1198,3 +1175,52 @@ class MyComponent extends Component {
The way this works is that sub templates are inlined, and have access to the
ambient context. They can therefore access `props`, and any other part of the
caller component.
### SVG Components
Owl components can be used to generate dynamic SVG graphs:
```js
class Node extends Component {
static template = xml`
<g>
<circle t-att-cx="props.x" t-att-cy="props.y" r="4" fill="black"/>
<text t-att-x="props.x - 5" t-att-y="props.y + 18"><t t-esc="props.node.label"/></text>
<t t-set="childx" t-value="props.x + 100"/>
<t t-set="height" t-value="props.height/(props.node.children || []).length"/>
<t t-foreach="props.node.children || []" t-as="child">
<t t-set="childy" t-value="props.y + child_index*height"/>
<line t-att-x1="props.x" t-att-y1="props.y" t-att-x2="childx" t-att-y2="childy" stroke="black" />
<Node x="childx" y="childy" node="child" height="height"/>
</t>
</g>
`;
static components = { Node };
}
class RootNode extends Component {
static template = xml`
<svg height="180">
<Node node="graph" x="10" y="20" height="180"/>
</svg>
`;
static components = { Node };
graph = {
label: "a",
children: [
{ label: "b" },
{ label: "c", children: [{ label: "d" }, { label: "e" }] },
{ label: "f", children: [{ label: "g" }] }
]
};
}
```
This `RootNode` component will then display a live SVG representation of the
graph described by the `graph` property. Note that there is a recursive structure
here: the `Node` component uses itself as a subcomponent.
Note that since SVG needs to be handled in a specific way (its namespace needs
to be properly set), there is a small constraint for Owl components: if an owl
component is supposed to be a part of an svg graph, then its root node needs to
be a `g` tag, so Owl can properly set the namespace.
+108
View File
@@ -0,0 +1,108 @@
# 🦉 Context 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [`Context`](#context)
- [`useContext`](#usecontext)
## Overview
The `Context` object provides a way to share data between an arbitrary number
of component. Usually, data is passed from a parent to its children component,
but when we have to deal with some mostly global information, this can be
annoying, since each component will need to pass the information to each children,
even though some or most of them will not use the information.
With a `Context` object, each component can subscribe (with the `useContext` hook)
to its state, and will be updated whenever the context state is updated.
## Example
Assume that we have an application with various components which needs to render
differently depending on the size of the device. Here is how we could proceed
to make sure that the information is properly shared. First, let us create a
context, and add it to the environment:
```js
const deviceContext = new Context({ isMobile: true });
const env = {
qweb: new QWeb(TEMPLATES),
deviceContext
};
```
If we want to make it completely responsive, we need to update its value whenever
the size of the screen is updated:
```js
const isMobile = () => window.innerWidth <= 768;
window.addEventListener(
"resize",
owl.utils.debounce(() => {
const state = deviceContext.state;
if (state.isMobile !== isMobile()) {
state.isMobile = !state.isMobile;
}
}, 15)
);
```
Then, each component that want can subscribe and render differently depending on the
fact that we are in a mobile or desktop mode.
```js
class SomeComponent extends Component {
static template = xml`
<div>
<t t-if=device.isMobile>
some simplified user interface
</t>
<t t-else="1">
some more sopthisticated user interface
</t>
`;
device = useContext(this.env.deviceContext);
}
```
## Reference
### `Context`
A `Context` object should be created with a state object:
```js
const someContext = new Context({ some: "key" });
```
Its state is now available in the `state` key:
```js
someContext.state.some = "other key";
```
This is the way some global code (such as the responsive code above) should
read and update the context state. However, components should not ever read the
context state directly from the context, they should instead use the `useContext`
hook to properly register themselves to state changes.
Note that the `Context` hook is different from the React version. For example,
there is no concept of provider/consumer. So, the `Context` feature does not
by itself allow the use of a different context state depending on the component
place in the component tree. However, this functionality can be obtained, if
necessary, with the use of sub environment.
### `useContext`
The `useContext` hook is the normal way for a component to register themselve
to context state changes. The `useContext` method returns the context state:
```js
device = useContext(this.env.deviceContext);
```
It is a simple observed state (with an owl `Observer`), which contains the shared
information.
+1 -1
View File
@@ -5,7 +5,7 @@ parts of the code. Owl has a very simple bus class, which manages subscriptions,
triggering events, and callbacks.
```js
const bus = new owl.EventBus();
const bus = new owl.core.EventBus();
bus.on("some-event", null, function(...args) {
console.log(...args);
+245 -12
View File
@@ -3,13 +3,24 @@
## Content
- [Overview](#overview)
- [Example](#example)
- [Example: Mouse Position](#example-mouse-position)
- [Example: Autofocus](#example-autofocus)
- [Reference](#reference)
- [One Rule](#one-rule)
- [`useState`](#usestate)
- [`onMounted`](#onmounted)
- [`onWillUnmount`](#onwillunmount)
- [`onWillPatch`](#onwillpatch)
- [`onPatched`](#onpatched)
- [`onWillStart`](#onwillstart)
- [`onWillUpdateProps`](#onwillupdateprops)
- [`useContext`](#usecontext)
- [`useRef`](#useref)
- [`useSubEnv`](#usesubenv)
- [`useStore`](#usestore)
- [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters)
- [Making customized hooks](#making-customized-hooks)
## Overview
@@ -28,7 +39,7 @@ Hooks works beautifully with Owl components: they solve the problems mentioned
above, and in particular, they are the perfect way to make your component
reactive.
## Example
## Example: mouse position
Here is the classical example of a non trivial hook to track the mouse position.
@@ -57,9 +68,9 @@ function useMouse() {
// Main root component
class App extends owl.Component {
static template = xml`
<div t-name="App">
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>`;
<div t-name="App">
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>`;
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
@@ -69,6 +80,49 @@ class App extends owl.Component {
Note that we use the prefix `use` for hooks, just like in React. This is just
a convention.
## Example: autofocus
Hooks can be combined to create the desired effect. For example, the following
hook combines the `useRef` hook with the `onPatched` and `onMounted` functions
to create an easy way to focus an input whenever it appears in the DOM:
```js
function useAutofocus(name) {
let ref = useRef(name);
let isInDom = false;
function updateFocus() {
if (!isInDom && ref.el) {
isInDom = true;
ref.el.focus();
} else if (isInDom && !ref.el) {
isInDom = false;
}
}
onPatched(updateFocus);
onMounted(updateFocus);
}
```
This hook takes the name of a valid `t-ref` directive, which should be present
in the template. It then checks whenever the component is mounted or patched if
the reference is not valid, and in this case, it will focus the node element.
This hook can be used like this:
```js
class SomeComponent extends Component {
static template = xml`
<div>
<input />
<input t-ref="myinput"/>
</div>`;
constructor(...args) {
super(...args);
useAutofocus("myinput");
}
}
```
## Reference
### One rule
@@ -79,25 +133,29 @@ constructor (or in class fields):
```js
// ok
class SomeComponent extends Component {
state = useState({value: 0});
state = useState({ value: 0 });
}
// also ok
class SomeComponent extends Component {
constructor(...args) {
super(...args);
this.state = useState({value: 0});
this.state = useState({ value: 0 });
}
}
// not ok: this is executed after the constructor is called
class SomeComponent extends Component {
async willStart() {
this.state = useState({value: 0});
this.state = useState({ value: 0 });
}
}
```
Hooks can get a reference to the currently being defined component with the
`Component.current` static property. This is why they need to be called in the
constructor.
### `useState`
The `useState` hook is certainly the most important hooks for Owl components:
@@ -125,16 +183,85 @@ class Counter extends owl.Component {
### `onMounted`
`onMounted` is not an hook, but is a building block designed to help make useful
hooks. `onMounted` register a callback, which will be called when the component
`onMounted` is not an user hook, but is a building block designed to help make useful
abstractions. `onMounted` registers a callback, which will be called when the component
is mounted (see example on top of this page).
### `onWillUnmount`
`onWillUnmount` is not an hook, but is a building block designed to help make useful
hooks. `onWillUnmount` register a callback, which will be called when the component
`onWillUnmount` is not an user hook, but is a building block designed to help make useful
abstractions. `onWillUnmount` registers a callback, which will be called when the component
is unmounted (see example on top of this page).
### `onWillPatch`
`onWillPatch` is not an user hook, but is a building block designed to help make useful
abstractions. `onWillPatch` registers a callback, which will be called just
before the component patched.
### `onPatched`
`onPatched` is not an user hook, but is a building block designed to help make useful
abstractions. `onPatched` registers a callback, which will be called just
after the component patched.
### `onWillStart`
`onWillStart` is an asynchronous hook. This means that the function registered
in the hook will be run just before the component is first rendered and can return a
promise, to express the fact that it is an asynchronous operation.
Note that if there are more than one `onWillStart` registered callback, then they
will all be run in parallel.
It can be used to load some initial data. For example, the following hook will
automatically load some data from the server, and return an object that will
be ready whenever the component is rendered:
```js
function useLoader() {
const component = Component.current;
const record = useState({});
onWillStart(async () => {
const recordId = component.props.id;
Object.assign(record, await fetchSomeRecord(recordId));
});
return record;
}
```
Note that this example does not update the record value whenever props are
updated. For that situation, we need to use the `onWillUpdateProps` hook.
### `onWillUpdateProps`
Just like `onWillStart`, `onWillUpdateProps` is an asynchronous hook. It is
designed to be run whenever the component props are updated. This could be
useful to perform some asynchronous task such as fetching updated data.
```js
function useLoader() {
const component = Component.current;
const record = useState({});
async function updateRecord(id) {
Object.assign(record, await fetchSomeRecord(id));
}
onWillStart(() => updateRecord(component.props.id));
onWillUpdateProps(nextProps => updateRecord(nextProps.id));
return record;
}
```
Note that if there are more than one `onWillUpdateProps` registered callback,
then they will all be run in parallel.
### `useContext`
See [`useContext`](context.md#usecontext) for reference documentation.
### `useRef`
The `useRef` hook is useful when we need a way to interact with some inside part
@@ -187,3 +314,109 @@ this.ref2 = useRef("component_2");
References are only guaranteed to be active while the parent component is mounted.
If this is not the case, accessing `el` or `comp` on it will return `null`.
### `useSubEnv`
The environment is sometimes useful to share some common information between
all components. But sometimes, we want to _scope_ that knowledge to a subtree.
For example, if we have a form view component, maybe we would like to make some
`model` object available to all sub component, but not to the whole application.
This is where the `useSubEnv` hook may be useful: it let a component add some
information to the environment in a way that only the component and its children
can access it:
```js
class FormComponent extends Component {
constructor(...args) {
super(...args);
const model = makeModel();
useSubEnv({ model });
}
}
```
The `useSubEnv` takes one argument: an object which contains some key/value that
will be added to the parent environment. Note that it will extend, not replace
the parent environment. And of course, the parent environment will not be
affected.
### `useStore`
The `useStore` hook is the entry point for a component to connect to the store.
See the [store documentation](store.md) for more information.
### `useDispatch`
The `useDispatch` hook is the way for components to get a reference to the store
`dispatch` function. See the [store documentation](store.md) for more information.
### `useGetters`
The `useGetters` hook is the way for components to get a reference to the store
getters. See the [store documentation](store.md) for more information.
### Making customized hooks
Hooks are a wonderful way to organize the code of a complex component by feature
instead of by lifecycle methods. They are like mixins, except that they can be
easily composed together.
But, like every good things in life, hooks should be used with moderation. They are
not the solution to every problem.
- they may be overkill: if your component needs to perform some action specific
to him (so, the specific code does not need to be shared), there is nothing
wrong with a simple class method:
```js
// maybe overkill
class A extends Component {
constructor(...args) {
super(...args);
useMySpecificHook()
}
}
// ok
class B extends Component {
constructor(...args) {
super(...args);
this.performSpecificTask()
}
}
```
Note that the second solution is easier to extend in sub components.
- they may be harder to test: if a customized hook inject some external side
effect dependency, then it is harder to test without doing some non obvious
manipulation. For example, assume that we want to give a reference to a
router in a `useRouter` hook. We could do this:
```js
const router = new Router(...);
function useRouter() {
return router;
}
```
As you can see, this does not *hook* into the internal of the component. It
simply returns a global object, which is difficult to mock.
A better way would be to do something like this: get the reference from the
environment.
```js
function useRouter() {
return Component.current.env.router;
}
```
This means that we give control to the application developer to create the
router, which is good, so they can set it up, subclass it, ... And then, to
test our components, we can just add a mock router in the environment.
Note: the code above makes use of the `Component.current` property. This is the
way hooks are able to get a reference to the component currently being created.
+5 -1
View File
@@ -72,7 +72,6 @@ needs. Here is a list of all Owl specific directives:
| `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) |
@@ -286,6 +285,11 @@ rendered with the value `value` set to `<span>foo</span>` in the rendering conte
<p><span>foo</span></p>
```
Note that since the content of the expression is not known beforehand, the `t-raw`
directive has to parse the html (and convert it to a virtual dom structure) for
each rendering. So, it will be much slower than a regular template. It is
therefore advised to limit the use of `t-raw` whenever possible.
### Setting Variables
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
+13 -3
View File
@@ -8,23 +8,31 @@ build applications. Here is a complete representation of its content:
```
owl
Component
Context
QWeb
Store
useState
core
EventBus
Observer
hooks
onWillStart
onMounted
onWillUpdateProps
onWillPatch
onPatched
onWillUnmount
useContext
useState
useRef
useSubEnv
useStore
useDispatch
useGetters
router
Link
RouteComponent
Router
store
Store
ConnectedComponent
tags
xml
utils
@@ -35,12 +43,14 @@ owl
whenReady
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
## Reference
- [Animations](animations.md)
- [Component](component.md)
- [Context](context.md)
- [Event Bus](event_bus.md)
- [Hooks](hooks.md)
- [Observer](observer.md)
+53 -52
View File
@@ -9,6 +9,7 @@
- [Actions](#actions)
- [Getters](#getters)
- [Connecting a Component](#connecting-a-component)
- [`useStore`](#usestore)
- [Semantics](#semantics)
- [Good Practices](#good-practices)
@@ -21,8 +22,9 @@ in various parts of the user interface, and then, it is not obvious which
component should own which part of the state.
Owl's solution to this issue is a centralized store. It is a class that owns
some state, and let the developer update it in a structured way, with `actions`.
Owl components can then connect to the store, and will be updated if necessary.
some (or all) state, and let the developer update it in a structured way, with
`actions`. Owl components can then connect to the store, and will be updated if
necessary.
Note: Owl store is inspired by React Redux and VueX.
@@ -47,7 +49,7 @@ const state = {
};
const store = new owl.Store({ state, actions });
store.on("update", () => console.log(store.state));
store.on("update", null, () => console.log(store.state));
// updating the state
store.dispatch("addTodo", "fix all bugs");
@@ -110,6 +112,14 @@ const actions = {
};
```
The first argument to an action method is an object with four keys:
- `state`: the current state of the store content,
- `dispatch`: a function that can be used to dispatch other actions,
- `getters`: an object containing all getters defined in the store
- `env`: the current environment. This is useful sometimes, in particular if
an action needs to apply some side effects (such as performing an rpc), and
the `rpc` method is located in the environment.
Actions are called with the `dispatch` method on the store, and can receive an
arbitrary number of arguments.
@@ -195,76 +205,66 @@ is a string or a number.
### Connecting a Component
At some point, we need a way to access the state in the store from a component.
By default, an Owl `Component` is not connected to any store. To do that, we
need to create a component inheriting from `OwlComponent`:
At some point, we need a way to interact with the store from a component. This
can be done with the help of the three store hooks:
- `useStore` to subscribe a component to some part of the store state,
- `useDispatch` to get a reference to a dispatch function
- `useGetters` to get a reference to the getters defined in the store.
Assume we have this store:
```javascript
const actions = {
increment({ state }, val) {
state.counter += val;
state.counter.value += val;
}
};
const state = {
counter: 0
counter: {value: 0}
};
const store = new owl.Store({ state, actions });
```
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
increment() {
this.env.store.dispatch("increment");
}
A counter component can then select this value and dispatch an action like this:
```js
class Counter extends Component {
counter = useStore(state => state.counter);
dispatch = useDispatch();
}
const counter = new Counter({ store, qweb });
```
```xml
<button t-name="Counter" t-on-click="increment">
Click Me! [<t t-esc="props.value"/>]
</button>
```
The `ConnectedComponent` class can be configured with the following fields:
- `mapStoreToProps`: a function that extracts the `props` of the Component
from the `state` of the `Store` and returns them as a dict.
- `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` (boolean): [only useful if no hashFunction is given] if `false`, only watch
for top level state changes (`true` by default)
Note that the class `ConnectedComponent` has a `dispatch` method. This means
that the previous example could be simplified like this:
```javascript
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
}
```
```xml
<button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="props.value"/>]
Click Me! [<t t-esc="counter.value"/>]
</button>
```
### `useStore``
The `useStore` hook is used to select some part of the store state. It accepts
two arguments:
- a selector function, which takes the store state as first argument (and the
component props as second argument) and returns
an object or an array (which will be then observed)
- optionally, an object with a `store` key (if we want to override the default
store) and an equality function (if we want to specialize the comparison)
If the `useStore` callback selects a sub part of the store state, the component
will only be rerendered whenever this part of the state changes. Otherwise, it
will perform a strict equality check and updates the component every time this
check fails.
### Semantics
The `Store` and the `ConnectedComponent` try to be smart and to optimize as much
The `Store` class and the `useStore` hook try to be smart and to optimize as much
as possible the rendering and update process. What is important to know is:
- components are always updated in the order of their creation (so, parent
@@ -282,8 +282,9 @@ as possible the rendering and update process. What is important to know is:
- avoid asynchronous components as much as possible. Asynchronous components
lead to situations where parts of the UI is not updated immediately.
- do not be afraid to connect many components, parent or children if needed. For
example, a `MessageList` component could get a list of ids in its `mapStoreToProps` and a `Message` component could get the data of its own
example, a `MessageList` component could get a list of ids in its `useStore`
call and a `Message` component could get the data of its own
message
- since the `mapStoreToProps` function is called for each connected component,
- since the `useStore` function is called for each connected component,
for each state update, it is important to make sure that these functions are
as fast as possible.
+1
View File
@@ -6,6 +6,7 @@
- [Development Mode](#development-mode)
- [Playground](#playground)
- [Benchmarks](#benchmarks)
- [Single File Component](#single-file-component)
## Overview
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "owl-framework",
"version": "0.22.0",
"version": "0.23.0",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"scripts": {
+71 -76
View File
@@ -49,7 +49,6 @@ export interface Fiber<Props> {
patchQueue: Fiber<any>[];
component: Component<any, any>;
vnode: VNode | null;
willPatchResult: any;
props: Props;
promise: Promise<VNode> | null;
// handlers?: any;
@@ -82,9 +81,13 @@ interface Internal<T extends Env, Props> {
boundHandlers: { [key: number]: any };
observer: Observer | null;
render: CompiledTemplate | null;
mountedHandlers: { [key: number]: Function };
render: CompiledTemplate;
mountedCB: Function | null;
willUnmountCB: Function | null;
willPatchCB: Function | null;
patchedCB: Function | null;
willStartCB: Function | null;
willUpdatePropsCB: Function | null;
classObj: { [key: string]: boolean } | null;
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
}
@@ -98,7 +101,7 @@ export class Component<T extends Env, Props extends {}> {
readonly __owl__: Internal<Env, Props>;
static template?: string | null = null;
static _template?: string | null = null;
static _current?: any | null = null;
static current: Component<any, any> | null = null;
static components = {};
static props?: any;
static defaultProps?: any;
@@ -139,7 +142,7 @@ export class Component<T extends Env, Props extends {}> {
*/
constructor(parent: Component<T, any> | T, props?: Props) {
const defaultProps = (<any>this.constructor).defaultProps;
Component._current = this;
Component.current = this;
if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps);
}
@@ -175,6 +178,8 @@ export class Component<T extends Env, Props extends {}> {
}
});
}
const qweb = this.env.qweb;
this.__owl__ = {
id: id,
vnode: null,
@@ -185,10 +190,14 @@ export class Component<T extends Env, Props extends {}> {
cmap: {},
currentFiber: null,
boundHandlers: {},
mountedHandlers: {},
mountedCB: null,
willUnmountCB: null,
willPatchCB: null,
patchedCB: null,
willStartCB: null,
willUpdatePropsCB: null,
observer: null,
render: null,
render: qweb.render.bind(qweb, this.__getTemplate(qweb)),
classObj: null,
refs: null
};
@@ -237,8 +246,6 @@ export class Component<T extends Env, Props extends {}> {
* It is not called on the initial render. This is useful to get some
* information which are in the DOM. For example, the current position of the
* scrollbar
*
* The return value of willPatch will be given to the patched function.
*/
willPatch(): any {}
@@ -254,10 +261,8 @@ export class Component<T extends Env, Props extends {}> {
* One need to be careful, because updates here will cause rerender, which in
* turn will cause other calls to updated. So, we need to be particularly
* careful at avoiding endless cycles.
*
* The snapshot parameter is the result of the call to willPatch.
*/
patched(snapshot: any) {}
patched() {}
/**
* willUnmount is a hook that is called each time just before a component is
@@ -352,25 +357,6 @@ export class Component<T extends Env, Props extends {}> {
}
}
__createFiber(force, scope, vars, parent?: Fiber<any>): Fiber<Props> {
const fiber: Fiber<Props> = {
force,
scope,
vars,
rootFiber: null,
isCancelled: false,
component: this,
vnode: null,
patchQueue: parent ? parent.patchQueue : [],
willPatchResult: null,
props: this.props,
promise: null
};
fiber.rootFiber = parent ? parent.rootFiber : fiber;
this.__owl__.currentFiber = fiber;
return fiber;
}
/**
* Destroy the component. This operation is quite complex:
* - it recursively destroy all children
@@ -400,26 +386,6 @@ export class Component<T extends Env, Props extends {}> {
return true;
}
/**
* This method is the correct way to update the environment of a component. Doing
* this will cause a full rerender of the component and its children, so this is
* an operation that should not be done frequently.
*
* A good usecase for updating the environment would be to update some mostly
* static config keys, such as a boolean to determine if we are in mobile
* mode or not.
*/
async updateEnv(nextEnv: Partial<T>): Promise<void> {
const __owl__ = this.__owl__;
if (__owl__.parent && __owl__.parent.env === this.env) {
this.env = Object.create(this.env);
}
Object.assign(this.env, nextEnv);
if (__owl__.isMounted) {
await this.render(true);
}
}
/**
* Emit a custom event of type 'eventType' with the given 'payload' on the
* component's el, if it exists. However, note that the event will only bubble
@@ -441,6 +407,27 @@ export class Component<T extends Env, Props extends {}> {
// Private
//--------------------------------------------------------------------------
/**
* This method is a helper to create a fiber element.
*/
__createFiber(force, scope, vars, parent?: Fiber<any>): Fiber<Props> {
const fiber: Fiber<Props> = {
force,
scope,
vars,
rootFiber: null,
isCancelled: false,
component: this,
vnode: null,
patchQueue: parent ? parent.patchQueue : [],
props: this.props,
promise: null
};
fiber.rootFiber = parent ? parent.rootFiber : fiber;
this.__owl__.currentFiber = fiber;
return fiber;
}
/**
* Private helper to perform a full destroy, from the point of view of an Owl
* component. It does not remove the el (this is done only once on the top
@@ -456,6 +443,9 @@ export class Component<T extends Env, Props extends {}> {
const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted;
if (isMounted) {
if (__owl__.willUnmountCB) {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.isMounted = false;
}
@@ -482,11 +472,10 @@ export class Component<T extends Env, Props extends {}> {
}
}
__owl__.isMounted = true;
const handlers = __owl__.mountedHandlers;
try {
this.mounted();
for (let key in handlers) {
handlers[key]();
if (__owl__.mountedCB) {
__owl__.mountedCB();
}
} catch (e) {
errorHandler(e, this);
@@ -525,7 +514,10 @@ export class Component<T extends Env, Props extends {}> {
if (defaultProps) {
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
}
await this.willUpdateProps(nextProps);
await Promise.all([
this.willUpdateProps(nextProps),
this.__owl__.willUpdatePropsCB && this.__owl__.willUpdatePropsCB(nextProps)
]);
this.props = nextProps;
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
fiber.patchQueue.push(fiber);
@@ -555,20 +547,8 @@ export class Component<T extends Env, Props extends {}> {
return fiber.promise;
}
async __prepareAndRender(fiber: Fiber<Props>): Promise<VNode> {
try {
await this.willStart();
} catch (e) {
errorHandler(e, this);
return Promise.resolve(h("div"));
}
const __owl__ = this.__owl__;
if (__owl__.isDestroyed) {
return Promise.resolve(h("div"));
}
const qweb = this.env.qweb;
__getTemplate(qweb: QWeb): string {
let p = (<any>this).constructor;
// console.warn(p, p.template, p._template, 'template' in p, p.hasOwnProperty('template'))
if (!p.hasOwnProperty("_template")) {
if (p.template) {
p._template = p.template;
@@ -588,7 +568,19 @@ export class Component<T extends Env, Props extends {}> {
}
}
}
__owl__.render = qweb.render.bind(qweb, p._template);
return p._template;
}
async __prepareAndRender(fiber: Fiber<Props>): Promise<VNode> {
try {
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
} catch (e) {
errorHandler(e, this);
return Promise.resolve(h("div"));
}
const __owl__ = this.__owl__;
if (__owl__.isDestroyed) {
return Promise.resolve(h("div"));
}
return this.__render(fiber);
}
@@ -603,7 +595,6 @@ export class Component<T extends Env, Props extends {}> {
vnode = __owl__.render!(this, {
promises,
handlers: __owl__.boundHandlers,
mountedHandlers: __owl__.mountedHandlers,
fiber: fiber
});
} catch (e) {
@@ -686,9 +677,11 @@ export class Component<T extends Env, Props extends {}> {
try {
const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
component = fiber.component;
fiber.willPatchResult = component.willPatch();
component = patchQueue[i].component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.willPatch();
}
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
@@ -696,9 +689,11 @@ export class Component<T extends Env, Props extends {}> {
component.__patch(fiber.vnode);
}
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
component.patched(fiber.willPatchResult);
component = patchQueue[i].component;
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
}
} catch (e) {
errorHandler(e, component);
+1 -1
View File
@@ -1,5 +1,5 @@
import { QWeb } from "../qweb/index";
import { INTERP_REGEXP } from "../qweb/context";
import { INTERP_REGEXP } from "../qweb/compilation_context";
import { MODS_CODE } from "../qweb/extensions";
//------------------------------------------------------------------------------
+95
View File
@@ -0,0 +1,95 @@
import { Component } from "./component/component";
import { Observer } from "./core/observer";
import { EventBus } from "./core/event_bus";
import { onWillUnmount } from "./hooks";
/**
* The `Context` object provides a way to share data between an arbitrary number
* of component. Usually, data is passed from a parent to its children component,
* but when we have to deal with some mostly global information, this can be
* annoying, since each component will need to pass the information to each
* children, even though some or most of them will not use the information.
*
* With a `Context` object, each component can subscribe (with the `useContext`
* hook) to its state, and will be updated whenever the context state is updated.
*/
export class Context extends EventBus {
state: any;
observer: Observer;
id: number = 1;
// mapping from component id to last observed context id
mapping: { [componentId: number]: number } = {};
constructor(state: Object = {}) {
super();
this.observer = new Observer();
this.observer.notifyCB = this.__notifyComponents.bind(this);
this.state = this.observer.observe(state);
}
/**
* Instead of using trigger to emit an update event, we actually implement
* our own function to do that. The reason is that we need to be smarter than
* a simple trigger function: we need to wait for parent components to be
* done before doing children components. The reason is that if an update
* as an effect of destroying a children, we do not want to call the
* mapStoreToProps function of the child, nor rendering it.
*
* This method is not optimal if we have a bunch of asynchronous components:
* we wait sequentially for each component to be completed before updating the
* next. However, the only things that matters is that children are updated
* after their parents. So, this could be optimized by being smarter, and
* updating all widgets concurrently, except for parents/children.
*
* A potential cheap way to improve this situation is to keep track of the
* depth of a component in the component tree. A root component has a depth of
* 1, then its children of 2 and so on... Then, we can update all components
* with the same depth in parallel.
*/
async __notifyComponents() {
const id = ++this.id;
const subs = this.subscriptions.update || [];
for (let i = 0, iLen = subs.length; i < iLen; i++) {
const sub = subs[i];
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
if (shouldCallback) {
await sub.callback.call(sub.owner, id);
}
}
}
}
/**
* The`useContext` hook is the normal way for a component to register themselve
* to context state changes. The `useContext` method returns the context state
*/
export function useContext(ctx: Context): any {
const component: Component<any, any> = Component.current!;
return useContextWithCB(ctx, component, component.render.bind(component));
}
export function useContextWithCB(ctx: Context, component, method): any {
const __owl__ = component.__owl__;
const id = __owl__.id;
const mapping = ctx.mapping;
if (id in mapping) {
return ctx.state;
}
mapping[id] = 0;
const renderFn = __owl__.render;
__owl__.render = function(comp, params) {
mapping[id] = ctx.id;
return renderFn(comp, params);
};
ctx.on("update", component, async contextId => {
if (mapping[id] < contextId) {
mapping[id] = contextId;
await method();
}
});
onWillUnmount(() => {
ctx.off("update", component);
delete mapping[id];
});
return ctx.state;
}
+74 -29
View File
@@ -12,15 +12,17 @@ import { Observer } from "./core/observer";
* - useRef
*/
// -----------------------------------------------------------------------------
// useState
// -----------------------------------------------------------------------------
/**
* useState hook
*
* This is the main way a component can be made reactive. The useState hook
* will return an observed object (or array). Changes to that value will then
* trigger a rerendering of the current component.
*/
export function useState<T>(state: T): T {
const component: Component<any, any> = Component._current;
const component: Component<any, any> = Component.current!;
const __owl__ = component.__owl__;
if (!__owl__.observer) {
__owl__.observer = new Observer();
@@ -29,37 +31,66 @@ export function useState<T>(state: T): T {
return __owl__.observer.observe(state);
}
/**
* Mounted hook. The callback will be called when the current component is
* mounted. Note that the component mounted method is called first.
*/
let nextID = 1;
export function onMounted(cb) {
const component: Component<any, any> = Component._current;
component.__owl__.mountedHandlers[`h${nextID++}`] = cb;
}
/**
* willUnmount hook. The callback will be called when the current component is
* willUnmounted. Note that the component mounted method is called last.
*/
export function onWillUnmount(cb) {
const component: Component<any, any> = Component._current;
if (component.__owl__.willUnmountCB) {
const current = component.__owl__.willUnmountCB;
component.__owl__.willUnmountCB = function() {
cb.call(component);
current.call(component);
// -----------------------------------------------------------------------------
// Life cycle hooks
// -----------------------------------------------------------------------------
function makeLifecycleHook(method: string, reverse: boolean = false) {
if (reverse) {
return function(cb) {
const component: Component<any, any> = Component.current!;
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function() {
current.call(component);
cb.call(component);
};
} else {
component.__owl__[method] = cb;
}
};
} else {
component.__owl__.willUnmountCB = cb;
return function(cb) {
const component: Component<any, any> = Component.current!;
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function() {
cb.call(component);
current.call(component);
};
} else {
component.__owl__[method] = cb;
}
};
}
}
function makeAsyncHook(method: string) {
return function(cb) {
const component: Component<any, any> = Component.current!;
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function(...args) {
return Promise.all[(current.call(component, ...args), cb.call(component, ...args))];
};
} else {
component.__owl__[method] = cb;
}
};
}
export const onMounted = makeLifecycleHook("mountedCB", true);
export const onWillUnmount = makeLifecycleHook("willUnmountCB");
export const onWillPatch = makeLifecycleHook("willPatchCB");
export const onPatched = makeLifecycleHook("patchedCB", true);
export const onWillStart = makeAsyncHook("willStartCB");
export const onWillUpdateProps = makeAsyncHook("willUpdatePropsCB");
// -----------------------------------------------------------------------------
// useRef
// -----------------------------------------------------------------------------
/**
* useRef hook
*
* The purpose of this hook is to allow components to get a reference to a sub
* html node or component.
*/
@@ -69,7 +100,7 @@ interface Ref {
}
export function useRef(name: string): Ref {
const __owl__ = Component._current.__owl__;
const __owl__ = Component.current!.__owl__;
return {
get el(): HTMLElement | null {
const val = __owl__.refs && __owl__.refs[name];
@@ -81,3 +112,17 @@ export function useRef(name: string): Ref {
}
};
}
// -----------------------------------------------------------------------------
// useSubEnv
// -----------------------------------------------------------------------------
/**
* This hook is a simple way to let components use a sub environment. Note that
* like for all hooks, it is important that this is only called in the
* constructor method.
*/
export function useSubEnv(nextEnv) {
const component = Component.current!;
component.env = Object.assign(Object.create(component.env), nextEnv);
}
+13 -7
View File
@@ -7,25 +7,31 @@
import { EventBus } from "./core/event_bus";
import { Observer } from "./core/observer";
import { QWeb } from "./qweb/index";
import { ConnectedComponent } from "./store/connected_component";
import { Store } from "./store/store";
import * as _store from "./store";
import * as _utils from "./utils";
import * as _tags from "./tags";
import * as _hooks from "./hooks";
import { Link } from "./router/Link";
import { RouteComponent } from "./router/RouteComponent";
import { Router } from "./router/Router";
import * as _context from "./context";
import { Link } from "./router/link";
import { RouteComponent } from "./router/route_component";
import { Router } from "./router/router";
export { Component } from "./component/component";
export { QWeb };
export const Context = _context.Context;
export const useState = _hooks.useState;
export const core = { EventBus, Observer };
export const router = { Router, RouteComponent, Link };
export const store = { Store, ConnectedComponent };
export const Store = _store.Store;
export const utils = _utils;
export const tags = _tags;
export const hooks = _hooks;
export const hooks = Object.assign({}, _hooks, {
useContext: _context.useContext,
useDispatch: _store.useDispatch,
useGetters: _store.useGetters,
useStore: _store.useStore
});
export const __info__ = {};
Object.defineProperty(__info__, "mode", {
+10 -22
View File
@@ -1,6 +1,7 @@
import { Context } from "./context";
import { CompilationContext } from "./compilation_context";
import { QWebExprVar } from "./expression_parser";
import { QWeb } from "./qweb";
import { htmlToVDOM } from "../vdom/html_to_vdom";
/**
* Owl QWeb Directives
@@ -25,7 +26,9 @@ QWeb.utils.getFragment = function(str: string): DocumentFragment {
return temp.content;
};
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
QWeb.utils.htmlToVDOM = htmlToVDOM;
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: CompilationContext) {
if (value === "0" && ctx.caller) {
qweb._compileNode(ctx.caller, ctx);
return;
@@ -55,18 +58,13 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
ctx.addLine(`result = vn${nodeID}`);
if (ctx.rootContext.shouldDefineResult) {
ctx.addLine(`result = vn${nodeID}`);
}
}
} else {
let fragID = ctx.generateID();
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`var frag${fragID} = utils.getFragment(${exprID})`);
let tempNodeID = ctx.generateID();
ctx.addLine(`var p${tempNodeID} = {hook: {`);
ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
ctx.addLine(`}};`);
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
}
if (node.childNodes.length) {
ctx.addElse();
@@ -80,11 +78,6 @@ QWeb.addDirective({
name: "esc",
priority: 70,
atNodeEncounter({ node, qweb, ctx }): boolean {
if (node.nodeName !== "t") {
let nodeID = qweb._compileGenericNode(node, ctx);
ctx = ctx.withParent(nodeID);
ctx = ctx.subContext("currentKey", ctx.lastNodeKey);
}
let value = ctx.getValue(node.getAttribute("t-esc")!);
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
return true;
@@ -95,11 +88,6 @@ QWeb.addDirective({
name: "raw",
priority: 80,
atNodeEncounter({ node, qweb, ctx }): boolean {
if (node.nodeName !== "t") {
let nodeID = qweb._compileGenericNode(node, ctx);
ctx = ctx.withParent(nodeID);
ctx = ctx.subContext("currentKey", ctx.lastNodeKey);
}
let value = ctx.getValue(node.getAttribute("t-raw")!);
compileValueNode(value, node, qweb, ctx);
return true;
@@ -199,7 +187,7 @@ QWeb.addDirective({
nodeCopy.removeAttribute("t-call");
// extract variables from nodecopy
const tempCtx = new Context();
const tempCtx = new CompilationContext();
tempCtx.nextID = ctx.rootContext.nextID;
tempCtx.allowMultipleRoots = true;
qweb._compileNode(nodeCopy, tempCtx);
@@ -5,7 +5,7 @@ export const INTERP_REGEXP = /\{\{.*?\}\}/g;
// Compilation Context
//------------------------------------------------------------------------------
export class Context {
export class CompilationContext {
nextID: number = 1;
code: string[] = [];
variables: { [key: string]: QWebVar } = {};
@@ -14,7 +14,7 @@ export class Context {
parentTextNode: number | null = null;
rootNode: number | null = null;
indentLevel: number = 0;
rootContext: Context;
rootContext: CompilationContext;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
shouldDefineParent: boolean = false;
@@ -87,7 +87,7 @@ export class Context {
return this.code;
}
withParent(node: number): Context {
withParent(node: number): CompilationContext {
if (
!this.allowMultipleRoots &&
this === this.rootContext &&
@@ -98,13 +98,13 @@ export class Context {
if (!this.rootContext.rootNode) {
this.rootContext.rootNode = node;
}
if (!this.parentNode) {
if (!this.parentNode && this.rootContext.shouldDefineResult) {
this.addLine(`result = vn${node};`);
}
return this.subContext("parentNode", node);
}
subContext(key: keyof Context, value: any): Context {
subContext(key: keyof CompilationContext, value: any): CompilationContext {
const newContext = Object.create(this);
newContext[key] = value;
return newContext;
+35 -49
View File
@@ -49,25 +49,35 @@ QWeb.addDirective({
);
ctx.closeIf();
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
let handler;
if (mods.length > 0) {
handler = `function (e) {`;
let handler = `function (e) {`;
handler += mods
.map(function(mod) {
return MODS_CODE[mod];
})
.join("");
handler += `context['${handlerName}'].call(${params}, e);}`;
if (!extraArgs) {
handler += `context['${handlerName}'].call(${params}, e);}`;
ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
);
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
} else {
const handlerKey = `handler${ctx.generateID()}`;
ctx.addLine(`const ${handlerKey} = context['${handlerName}'].bind(${params});`);
handler += `${handlerKey}(e);}`;
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
}
} else {
handler = `context['${handlerName}'].bind(${params})`;
}
if (extraArgs) {
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
} else {
ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
);
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
const handler = `context['${handlerName}'].bind(${params})`;
if (extraArgs) {
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
} else {
ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
);
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
}
}
}
});
@@ -83,6 +93,7 @@ QWeb.addDirective({
const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`);
}
});
@@ -183,42 +194,6 @@ QWeb.addDirective({
}
});
//------------------------------------------------------------------------------
// t-mounted
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "mounted",
priority: 97,
atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
ctx.rootContext.shouldDefineOwner = true;
const eventName = fullName.slice(5);
if (!eventName) {
throw new Error("Missing event name with t-on directive");
}
let extraArgs;
let handler = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
/`/g,
"'"
)}'\`)})()`;
if (extraArgs) {
ctx.addLine(
`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
extraArgs
)});`
);
} else {
ctx.addLine(
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
);
}
addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
}
});
//------------------------------------------------------------------------------
// t-slot
//------------------------------------------------------------------------------
@@ -232,9 +207,20 @@ QWeb.addDirective({
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
);
ctx.addIf(`slot${slotKey}`);
let parentNode = `c${ctx.parentNode}`;
if (!ctx.parentNode) {
ctx.rootContext.shouldDefineResult = true;
ctx.rootContext.shouldDefineUtils = true;
parentNode = `children${ctx.nextID++}`;
ctx.addLine(`let ${parentNode}= []`);
ctx.addLine(`result = {}`);
}
ctx.addLine(
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: extra.vars, parent: owner}));`
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: owner}));`
);
if (!ctx.parentNode) {
ctx.addLine(`Promise.all(extra.promises).then(() => utils.defineProxy(result, ${parentNode}[0]))`);
}
ctx.closeIf();
return true;
}
+28 -13
View File
@@ -1,6 +1,6 @@
import { EventBus } from "../core/event_bus";
import { h, patch, VNode } from "../vdom/index";
import { Context } from "./context";
import { CompilationContext } from "./compilation_context";
import { shallowEqual } from "../utils";
import { addNS } from "../vdom/vdom";
@@ -36,7 +36,7 @@ interface Template {
interface CompilationInfo {
node: Element;
qweb: QWeb;
ctx: Context;
ctx: CompilationContext;
fullName: string;
value: string;
}
@@ -69,7 +69,8 @@ const whitespaceRE = /\s+/g;
const NODE_HOOKS_PARAMS = {
create: "(_, n)",
insert: "vn",
remove: "(vn, rm)"
remove: "(vn, rm)",
destroy: "()"
};
interface Utils {
@@ -103,8 +104,6 @@ const UTILS: Utils = {
function parseXML(xml: string): Document {
const parser = new DOMParser();
// we remove comments from the xml string
xml = xml.replace(/<!--[\s\S]*?-->/g, "");
const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) {
let msg = "Invalid XML in template.";
@@ -342,9 +341,9 @@ export class QWeb extends EventBus {
});
}
_compile(name: string, elem: Element, parentContext?: Context): CompiledTemplate {
_compile(name: string, elem: Element, parentContext?: CompilationContext): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new Context(name);
const ctx = new CompilationContext(name);
if (elem.tagName !== "t") {
ctx.shouldDefineResult = false;
}
@@ -409,7 +408,7 @@ export class QWeb extends EventBus {
* Generate code from an xml node
*
*/
_compileNode(node: ChildNode, ctx: Context) {
_compileNode(node: ChildNode, ctx: CompilationContext) {
if (!(node instanceof Element)) {
// this is a text node, there are no directive to apply
let text = node.textContent!;
@@ -420,7 +419,11 @@ export class QWeb extends EventBus {
text = text.replace(whitespaceRE, " ");
}
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
if (node.nodeType === 3) {
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
} else if (node.nodeType === 8) {
ctx.addLine(`c${ctx.parentNode}.push(h('!', \`${text}\`));`);
}
} else if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
} else {
@@ -449,8 +452,6 @@ export class QWeb extends EventBus {
fullName: string;
}[] = [];
let withHandlers = false;
// maybe this is not optimal: we iterate on all attributes here, and again
// just after for each directive.
for (let i = 0; i < attributes.length; i++) {
@@ -460,11 +461,21 @@ export class QWeb extends EventBus {
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
throw new Error(`Unknown QWeb directive: '${attrName}'`);
}
if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) {
const tNode = document.createElement("t");
tNode.setAttribute(attrName, node.getAttribute(attrName)!);
for (let child of Array.from(node.childNodes)) {
tNode.appendChild(child);
}
node.appendChild(tNode);
node.removeAttribute(attrName);
}
}
}
const DIR_N = QWeb.DIRECTIVES.length;
const ATTR_N = attributes.length;
let withHandlers = false;
for (let i = 0; i < DIR_N; i++) {
let directive = QWeb.DIRECTIVES[i];
let fullName;
@@ -560,7 +571,11 @@ export class QWeb extends EventBus {
}
}
_compileGenericNode(node: ChildNode, ctx: Context, withHandlers: boolean = true): number {
_compileGenericNode(
node: ChildNode,
ctx: CompilationContext,
withHandlers: boolean = true
): number {
// nodeType 1 is generic tag
if (node.nodeType !== 1) {
throw new Error("unsupported node type");
@@ -723,7 +738,7 @@ export class QWeb extends EventBus {
return nodeID;
}
_compileChildren(node: ChildNode, ctx: Context) {
_compileChildren(node: ChildNode, ctx: CompilationContext) {
if (node.childNodes.length > 0) {
for (let child of Array.from(node.childNodes)) {
this._compileNode(child, ctx);
+1 -1
View File
@@ -1,6 +1,6 @@
import { Component } from "../component/component";
import { xml } from "../tags";
import { Destination, RouterEnv } from "./Router";
import { Destination, RouterEnv } from "./router";
type Props = Destination;
+142
View File
@@ -0,0 +1,142 @@
import { Component } from "./component/component";
import { Env } from "./component/component";
import { Context, useContextWithCB } from "./context";
import { onWillUpdateProps } from "./hooks";
/**
* Owl Store
*
* We have here:
* - a Store class
* - useStore hook
* - useDispatch hook
* - useGetters hook
*
* The Owl store is our answer to the problem of managing complex state across
* components. The main idea is that the store owns some state, allow external
* code to modify it through actions, and for each state changes,
* connected component will be notified, and updated if necessary.
*
* Note that this code is partly inspired by VueX and React/Redux
*/
//------------------------------------------------------------------------------
// Store Definition
//------------------------------------------------------------------------------
export type Action = ({ state, dispatch, env, getters }, ...payload: any) => any;
export type Getter = ({ state: any, getters }, payload?) => any;
interface StoreConfig {
env?: Env;
state?: any;
actions?: { [name: string]: Action };
getters?: { [name: string]: Getter };
}
export class Store extends Context {
actions: any;
env: any;
getters: { [name: string]: (payload?) => any };
updateFunctions: { [key: number]: (() => boolean)[] };
constructor(config: StoreConfig) {
super(config.state);
this.actions = config.actions;
this.env = config.env;
this.getters = {};
this.updateFunctions = [];
if (config.getters) {
const firstArg = {
state: this.state,
getters: this.getters
};
for (let g in config.getters) {
this.getters[g] = config.getters[g].bind(this, firstArg);
}
}
}
dispatch(action: string, ...payload: any): Promise<void> | void {
if (!this.actions[action]) {
throw new Error(`[Error] action ${action} is undefined`);
}
const result = this.actions[action](
{
dispatch: this.dispatch.bind(this),
env: this.env,
state: this.state,
getters: this.getters
},
...payload
);
return result;
}
}
interface SelectorOptions {
store?: Store;
isEqual?: (a: any, b: any) => boolean;
}
const isStrictEqual = (a, b) => a === b;
export function useStore(selector, options: SelectorOptions = {}): any {
const component: Component<any, any> = Component.current!;
const store = options.store || (component.env.store as Store);
let result = selector(store.state, component.props);
const hashFn = store.observer.deepRevNumber.bind(store.observer);
let revNumber = hashFn(result) || result;
const isEqual = options.isEqual || isStrictEqual;
if (!store.updateFunctions[component.__owl__.id]) {
store.updateFunctions[component.__owl__.id] = [];
}
const updateFunctions = store.updateFunctions[component.__owl__.id];
updateFunctions.push(function(): boolean {
const oldResult = result;
result = selector(store!.state, component.props);
const newRevNumber = hashFn(result);
if (
(newRevNumber > 0 && revNumber !== newRevNumber) ||
(newRevNumber === 0 && !isEqual(oldResult, result))
) {
revNumber = newRevNumber;
return true;
}
return false;
})
useContextWithCB(store, component, function(): Promise<void> | void {
let shouldRender = false;
updateFunctions.forEach(function (updateFn) {
shouldRender = updateFn() || shouldRender;
});
if (shouldRender) {
return component.render();
}
});
onWillUpdateProps(props => {
// FIXME: only do that if not keepalive + do it in destroy in that case
delete store.updateFunctions[component.__owl__.id];
result = selector(store.state, props);
});
return new Proxy(result, {
get(target, k) {
return result[k];
},
set(target, k, v) {
result[k] = v;
return true;
}
});
}
export function useDispatch(store?: Store): Store["dispatch"] {
store = store || (Component.current!.env.store as Store);
return store.dispatch.bind(store);
}
export function useGetters(store?: Store): Store["getters"] {
store = store || (Component.current!.env.store as Store);
return store.getters;
}
-147
View File
@@ -1,147 +0,0 @@
import { Component, Env, Fiber } from "../component/component";
import { VNode } from "../vdom/index";
//------------------------------------------------------------------------------
// Connect function
//------------------------------------------------------------------------------
type HashFunction = (a: any, b: any) => number;
export class ConnectedComponent<T extends Env, P> extends Component<T, P> {
deep: boolean = true;
getStore(env) {
return env.store;
}
storeProps: any;
hashFunction: HashFunction = (storeProps, options) => {
const revFn = (this.__owl__ as any).revFn;
const rev = revFn(storeProps);
if (rev > 0) {
return rev;
}
let hash = 0;
for (let key in storeProps) {
const val = storeProps[key];
const hashVal = revFn(val);
if (hashVal === 0) {
if (val !== options.prevStoreProps[key]) {
options.didChange = true;
}
} else {
hash += hashVal;
}
}
return hash;
};
static mapStoreToProps(storeState, ownProps, getters) {
return {};
}
dispatch(name, ...payload) {
return (this.__owl__ as any).store.dispatch(name, ...payload);
}
/**
* Need to do this here so 'deep' can be overrided by subcomponent easily
*/
async __prepareAndRender(fiber: Fiber<P>): Promise<VNode> {
const store = this.getStore(this.env);
const ownProps = this.props || {};
this.storeProps = (<any>this.constructor).mapStoreToProps(store.state, ownProps, store.getters);
const observer = store.observer;
const revFn = this.deep ? observer.deepRevNumber : observer.revNumber;
(this.__owl__ as any).store = store;
(this.__owl__ as any).ownProps = this.props;
(this.__owl__ as any).revFn = revFn.bind(observer);
(this.__owl__ as any).storeHash = this.hashFunction(this.storeProps, {
prevStoreProps: this.storeProps
});
(this.__owl__ as any).rev = observer.rev;
return super.__prepareAndRender(fiber);
}
/**
* We do not use the mounted hook here for a subtle reason: we want the
* updates to be called for the parents before the children. However,
* if we use the mounted hook, this will be done in the reverse order.
*/
__callMounted() {
(this.__owl__ as any).store.on("update", this, this.__checkUpdate);
super.__callMounted();
}
__callWillUnmount() {
(this.__owl__ as any).store.off("update", this);
super.__callWillUnmount();
}
__destroy(parent: any) {
(this.__owl__ as any).store.off("update", this);
super.__destroy(parent);
}
async render(force: boolean = false) {
this.__updateStoreProps(this.props);
// this is quite technical, so this deserves some explanation.
// When we have a connected component, it can be updated for 3 reasons:
// - some internal state changes (this will go through this method)
// - some props changes (if a parent is changed and need to rerender itself)
// - a store update
//
// It is possible (with connected component and parent) to have the following
// situation: the parent component is rendered first (from its state change),
// then immediately after, it is rendered (from store update). Then, if the
// __checkUpdate method is immediately over, the children component will
// be rendered again by the store update, even though it is supposed to be
// destroyed by the first rendering.
//
// So, the solution is to keep the information that there is a current
// rendering occuring with the same store state, the same props, and return
// that in the __checkUpdate method. To do this, we use the renderPromise
// deferred, which is not used by the component system once the
// component is ready, so we can use it for our own purpose.
(this.__owl__ as any).renderPromise = super.render(force);
return (this.__owl__ as any).renderPromise;
}
async __updateProps(nextProps: P, f, s, v) {
this.__updateStoreProps(nextProps);
return super.__updateProps(nextProps, f, s, v);
}
__updateStoreProps(nextProps): boolean {
const __owl__ = this.__owl__ as any;
const store = __owl__.store;
const observer = store.observer;
if (observer.rev === __owl__.rev && nextProps === __owl__.ownProps) {
return false;
}
const storeProps = (<any>this.constructor).mapStoreToProps(
store.state,
nextProps,
store.getters
);
const options = { prevStoreProps: this.storeProps, didChange: false };
const storeHash = this.hashFunction(storeProps, options);
this.storeProps = storeProps;
let didChange = options.didChange;
if (storeHash !== __owl__.storeHash) {
__owl__.storeHash = storeHash;
didChange = true;
}
__owl__.rev = store.observer.rev;
__owl__.ownProps = nextProps;
return didChange;
}
async __checkUpdate() {
const didChange = this.__updateStoreProps(this.props);
if (didChange) {
return this.render();
}
// see note in render method
return (this.__owl__ as any).renderPromise;
}
}
View File
-107
View File
@@ -1,107 +0,0 @@
import { Env } from "../component/component";
import { EventBus } from "../core/event_bus";
import { Observer } from "../core/observer";
/**
* Owl Store
*
* We have here:
* - a Store class
* - the ConnectedComponent class
*
* The Owl store is our answer to the problem of managing complex state across
* components. The main idea is that the store owns some state, allow external
* code to modify it through actions, and for each state changes,
* connected component will be notified, and updated if necessary.
*
* Note that this code is partly inspired by VueX and React/Redux
*/
//------------------------------------------------------------------------------
// Store Definition
//------------------------------------------------------------------------------
export type Action = ({ state, dispatch, env, getters }, ...payload: any) => any;
export type Getter = ({ state: any, getters }, payload?) => any;
interface StoreConfig {
env?: Env;
state?: any;
actions?: { [name: string]: Action };
getters?: { [name: string]: Getter };
}
interface StoreOption {
debug?: boolean;
}
export class Store extends EventBus {
state: any;
actions: any;
mutations: any;
debug: boolean;
env: any;
observer: Observer;
getters: { [name: string]: (payload?) => any };
constructor(config: StoreConfig, options: StoreOption = {}) {
super();
this.debug = options.debug || false;
this.actions = config.actions;
this.env = config.env;
this.observer = new Observer();
this.observer.notifyCB = this.__notifyComponents.bind(this);
this.state = this.observer.observe(config.state || {});
this.getters = {};
if (config.getters) {
const firstArg = {
state: this.state,
getters: this.getters
};
for (let g in config.getters) {
this.getters[g] = config.getters[g].bind(this, firstArg);
}
}
}
dispatch(action: string, ...payload: any): Promise<void> | void {
if (!this.actions[action]) {
throw new Error(`[Error] action ${action} is undefined`);
}
const result = this.actions[action](
{
dispatch: this.dispatch.bind(this),
env: this.env,
state: this.state,
getters: this.getters
},
...payload
);
return result;
}
/**
* Instead of using trigger to emit an update event, we actually implement
* our own function to do that. The reason is that we need to be smarter than
* a simple trigger function: we need to wait for parent components to be
* done before doing children components. The reason is that if an update
* as an effect of destroying a children, we do not want to call the
* mapStoreToProps function of the child, nor rendering it.
*
* This method is not optimal if we have a bunch of asynchronous components:
* we wait sequentially for each component to be completed before updating the
* next. However, the only things that matters is that children are updated
* after their parents. So, this could be optimized by being smarter, and
* updating all widgets concurrently, except for parents/children.
*/
async __notifyComponents() {
const subs = this.subscriptions.update || [];
for (let i = 0, iLen = subs.length; i < iLen; i++) {
const sub = subs[i];
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
if (shouldCallback) {
await sub.callback.call(sub.owner);
}
}
}
}
+29
View File
@@ -0,0 +1,29 @@
import { VNode, h } from "./vdom";
const parser = new DOMParser();
export function htmlToVDOM(html: string): VNode[] {
const doc = parser.parseFromString(html, "text/html");
const result: VNode[] = [];
for (let child of doc.body.childNodes) {
result.push(htmlToVNode(child));
}
return result;
}
function htmlToVNode(node: ChildNode): VNode {
if (!(node instanceof Element)) {
return { text: node.textContent! } as VNode;
}
const attrs = {};
for (let attr of node.attributes) {
attrs[attr.name] = attr.textContent;
}
const children: VNode[] = [];
if (node.hasChildNodes) {
for (let c of node.childNodes) {
children.push(htmlToVNode(c));
}
}
return h((node as Element).tagName, { attrs }, children);
}
@@ -10,7 +10,6 @@ exports[`animations t-transition combined with component 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
@@ -55,7 +54,6 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
if (context['state'].display) {
//COMPONENT
let def3;
@@ -99,7 +97,6 @@ exports[`animations t-transition with no delay/duration 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
result = vn1;
p1.hook = {
insert: vn => {
utils.transitionInsert(vn, 'jupiler');
@@ -120,7 +117,6 @@ exports[`animations t-transition, on a simple node (insert) 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
result = vn1;
p1.hook = {
insert: vn => {
utils.transitionInsert(vn, 'chimay');
@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`various scenarios scenarios with async store updates and some components events 1`] = `"<div><button>Do stuff</button><div><span>Attachment 100</span><span>Name: text.txt</span></div></div>"`;
exports[`various scenarios scenarios with async store updates and some components events 2`] = `"<div><button>Do stuff</button></div>"`;
-23
View File
@@ -5,7 +5,6 @@ import {
makeDeferred,
makeTestFixture,
makeTestEnv,
nextTick,
patchNextFrame,
renderToDOM,
unpatchNextFrame
@@ -265,28 +264,6 @@ describe("animations", () => {
expect(fixture.innerHTML).toBe("<div></div>");
});
test("t-transition combined with t-mounted", async () => {
env.qweb.addTemplate(
"TestWidget",
`<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
);
class TestWidget extends Widget {
state = useState({ flag: false });
f() {}
}
const widget = new TestWidget(env);
widget.f = jest.fn();
await widget.mount(fixture);
patchNextFrame(cb => cb());
expect(widget.f).toHaveBeenCalledTimes(0);
widget.state.flag = true;
await nextTick();
expect(widget.f).toHaveBeenCalledTimes(1);
unpatchNextFrame();
});
test("t-transition, remove and re-add before transitionend", async () => {
expect.assertions(11);
@@ -10,7 +10,6 @@ exports[`async rendering delayed component with t-asyncroot directive 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('button', p2, c2);
c1.push(vn2);
@@ -93,7 +92,6 @@ exports[`async rendering fast component with t-asyncroot directive 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('button', p2, c2);
c1.push(vn2);
@@ -176,7 +174,6 @@ exports[`async rendering t-component with t-asyncroot directive: mixed re-render
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('button', p2, c2);
c1.push(vn2);
@@ -259,7 +256,6 @@ exports[`class and style attributes with t-component dynamic t-att-style is prop
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
const _5 = context['state'].style;
@@ -303,7 +299,6 @@ exports[`class and style attributes with t-component t-att-class is properly add
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
const ref5 = \`child\`;
@@ -348,7 +343,6 @@ exports[`class and style attributes with t-component t-att-class is properly add
Object.assign(_2, utils.toObj({d:context['state'].d}))
let c3 = [], p3 = {key:3,class:_2};
var vn3 = h('span', p3, c3);
result = vn3;
return vn3;
}"
`;
@@ -364,7 +358,6 @@ exports[`class and style attributes with t-component t-att-class is properly add
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
const ref5 = \`child\`;
@@ -409,7 +402,6 @@ exports[`class and style attributes with t-component t-att-class is properly add
Object.assign(_2, utils.toObj(context['state'].d?'d':''))
let c3 = [], p3 = {key:3,class:_2};
var vn3 = h('span', p3, c3);
result = vn3;
return vn3;
}"
`;
@@ -424,7 +416,6 @@ exports[`class and style attributes with t-component t-att-class is properly add
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let _5 = {a:context['state'].a,b:context['state'].b};
@@ -468,7 +459,6 @@ exports[`composition sub components dom state with t-keepalive is preserved 1`]
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
if (context['state'].ok) {
//COMPONENT
let def3;
@@ -514,7 +504,6 @@ exports[`composition sub components with some state rendered in a loop 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = context['state'].numbers;
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2;
@@ -573,7 +562,6 @@ exports[`composition t-component with dynamic value 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
@@ -615,7 +603,6 @@ exports[`composition t-component with dynamic value 2 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
@@ -657,7 +644,6 @@ exports[`dynamic t-props basic use 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
@@ -699,7 +685,6 @@ exports[`lifecycle hooks willPatch/patched hook with t-keepalive 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
if (context['state'].flag) {
//COMPONENT
let def3;
@@ -744,7 +729,6 @@ exports[`other directives with t-component t-on with handler bound to argument 1
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
@@ -786,7 +770,6 @@ exports[`other directives with t-component t-on with handler bound to empty obje
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
@@ -828,7 +811,6 @@ exports[`other directives with t-component t-on with handler bound to empty obje
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
@@ -870,7 +852,6 @@ exports[`other directives with t-component t-on with handler bound to object 1`]
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
@@ -912,7 +893,6 @@ exports[`other directives with t-component t-on with prevent and self modifiers
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
@@ -954,7 +934,6 @@ exports[`other directives with t-component t-on with self and prevent modifiers
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
@@ -996,7 +975,6 @@ exports[`other directives with t-component t-on with self modifier 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
@@ -1038,7 +1016,6 @@ exports[`other directives with t-component t-on with stop and/or prevent modifie
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
@@ -1076,7 +1053,6 @@ exports[`random stuff/miscellaneous can inject values in tagged templates 1`] =
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
@@ -1098,7 +1074,6 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let key5 = 'key' + 'somestring';
let def3;
@@ -1143,7 +1118,6 @@ exports[`random stuff/miscellaneous t-on with handler bound to dynamic argument
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = context['props'].items;
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2;
@@ -1199,7 +1173,6 @@ exports[`t-model directive .lazy modifier 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('input', p2, c2);
c1.push(vn2);
@@ -1223,7 +1196,6 @@ exports[`t-model directive basic use, on an input 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('input', p2, c2);
c1.push(vn2);
@@ -1247,7 +1219,6 @@ exports[`t-model directive basic use, on another key in component 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('input', p2, c2);
c1.push(vn2);
@@ -1271,7 +1242,6 @@ exports[`t-model directive on a select 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('select', p2, c2);
c1.push(vn2);
@@ -1316,7 +1286,6 @@ exports[`t-model directive on a sub state key 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('input', p2, c2);
c1.push(vn2);
@@ -1340,7 +1309,6 @@ exports[`t-model directive on an input type=radio 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = 'radio';
var _3 = 'one';
var _4 = 'One';
@@ -1377,7 +1345,6 @@ exports[`t-model directive on an input, type=checkbox 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = 'checkbox';
let c3 = [], p3 = {key:3,attrs:{type: _2},on:{}};
var vn3 = h('input', p3, c3);
@@ -1408,7 +1375,6 @@ exports[`t-slot directive can define and call slots 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
@@ -1448,7 +1414,6 @@ exports[`t-slot directive can define and call slots 2`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2};
var vn2 = h('div', p2, c2);
c1.push(vn2);
@@ -1571,6 +1536,9 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
create: (_, n) => {
context.__owl__.refs[ref11] = n.elm;
},
destroy: () => {
delete context.__owl__.refs[ref11];
},
};
c10.push({text: \`do something\`});
}"
@@ -1603,7 +1571,6 @@ exports[`t-slot directive slots are rendered with proper context, part 2 1`] = `
var _1 = context['props'].to;
let c2 = [], p2 = {key:2,attrs:{href: _1}};
var vn2 = h('a', p2, c2);
result = vn2;
const slot3 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot3) {
slot3.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c2, vars: extra.vars, parent: owner}));
@@ -1624,7 +1591,6 @@ exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2};
var vn2 = h('u', p2, c2);
c1.push(vn2);
@@ -1707,7 +1673,6 @@ exports[`t-slot directive slots are rendered with proper context, part 3 1`] = `
var _1 = context['props'].to;
let c2 = [], p2 = {key:2,attrs:{href: _1}};
var vn2 = h('a', p2, c2);
result = vn2;
const slot3 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot3) {
slot3.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c2, vars: extra.vars, parent: owner}));
@@ -1728,7 +1693,6 @@ exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2};
var vn2 = h('u', p2, c2);
c1.push(vn2);
@@ -1813,7 +1777,6 @@ exports[`t-slot directive slots are rendered with proper context, part 4 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = 'User '+context['state'].user.name;
//COMPONENT
let def4;
@@ -1860,6 +1823,24 @@ exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
}"
`;
exports[`t-slot directive template can just return a slot 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let owner = context;
let result;
var h = this.h;
const slot1 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot1) {
let children2= []
result = {}
slot1.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: children2, vars: extra.vars, parent: owner}));
Promise.all(extra.promises).then(() => utils.defineProxy(result, children2[0]))
}
return result;
}"
`;
exports[`top level sub widgets basic use 1`] = `
"function anonymous(context,extra
) {
@@ -14,7 +14,6 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
+66 -189
View File
@@ -89,9 +89,8 @@ describe("basic widget properties", () => {
test("crashes if it cannot find a template", async () => {
expect.assertions(1);
class SomeWidget extends Component<any, any> {}
const widget = new SomeWidget(env);
try {
await widget.mount(fixture);
new SomeWidget(env);
} catch (e) {
expect(e.message).toBe('Could not find template for component "SomeWidget"');
}
@@ -625,24 +624,6 @@ describe("lifecycle hooks", () => {
expect(n).toBe(1);
});
test("patched hook is called after updateEnv", async () => {
let n = 0;
class TestWidget extends Widget {
state = useState({ a: 1 });
patched() {
n++;
}
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(n).toBe(0);
await widget.updateEnv({ isMobile: true });
expect(n).toBe(1);
});
test("shouldUpdate hook prevent rerendering", async () => {
let shouldUpdate = false;
class TestWidget extends Widget {
@@ -725,10 +706,8 @@ describe("lifecycle hooks", () => {
state = useState({ n: 1 });
willPatch() {
steps.push("parent:willPatch");
return "leffe";
}
patched(snapshot) {
expect(snapshot).toBe("leffe");
patched() {
steps.push("parent:patched");
}
}
@@ -2817,107 +2796,6 @@ describe("async rendering", () => {
});
});
describe("updating environment", () => {
test("can update widget env", async () => {
const widget = new Widget(env);
expect(widget.env).toBe(env);
await widget.updateEnv(<any>{ somekey: 4 });
expect(widget.env).toBe(env);
expect((<any>widget).env.somekey).toBe(4);
});
test("updating widget env does not render widget (if not mounted)", async () => {
let n = 0;
class TestWidget extends Widget {
__render(f) {
n++;
return super.__render(f);
}
}
const widget = new TestWidget(env);
expect(n).toBe(0);
await widget.updateEnv(<any>{ somekey: 4 });
expect(n).toBe(0);
await widget.mount(fixture);
expect(n).toBe(1);
await widget.updateEnv(<any>{ somekey: 5 });
expect(n).toBe(2);
widget.unmount();
expect(n).toBe(2);
await widget.updateEnv(<any>{ somekey: 5 });
expect(n).toBe(2);
});
test("updating child env does not modify parent env", async () => {
env.qweb.addTemplate("ParentWidget", `<div><t t-component="child"/></div>`);
class ParentWidget extends Widget {
static components = { child: Widget };
}
const parent = new ParentWidget(env);
await parent.mount(fixture);
const child = children(parent)[0];
expect(child.env).toBe(parent.env);
await child.updateEnv(<any>{ somekey: 4 });
expect(child.env).not.toBe(parent.env);
expect((<any>parent).env.somekey).toBeUndefined();
});
test("updating parent env does modify child env", async () => {
env.qweb.addTemplate("ParentWidget", `<div><t t-component="child"/></div>`);
class ParentWidget extends Widget {
static components = { child: Widget };
}
const parent = new ParentWidget(env);
await parent.mount(fixture);
const child = children(parent)[0];
expect(child.env.somekey).toBeUndefined();
await parent.updateEnv({ somekey: 4 });
expect(child.env.somekey).toBe(4);
});
test("updating parent env does modify child env, part 2", async () => {
env.qweb.addTemplate("ParentWidget", `<div><Child/></div>`);
class ParentWidget extends Widget {
static components = { Child: Widget };
}
const parent = new ParentWidget(env);
await parent.mount(fixture);
const child = children(parent)[0];
expect(child.env.somekey).toBeUndefined();
await child.updateEnv({ somekey: 4 });
await parent.updateEnv({ someotherkey: 4 });
expect(child.env.someotherkey).toBe(4);
});
test("updating env force a rerender", async () => {
env.qweb.addTemplate("TestWidget", `<div><t t-esc="env.someKey"/></div>`);
class TestWidget extends Widget {}
(<any>env).someKey = "hey";
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
await widget.updateEnv(<any>{ someKey: "rerendered" });
expect(fixture.innerHTML).toBe("<div>rerendered</div>");
});
test("updating env force rerendering children", async () => {
env.qweb.addTemplate("Parent", `<div><Child /></div>`);
class Child extends Widget {}
class Parent extends Widget {
static components = { Child };
}
env.qweb.addTemplate("Child", `<div><t t-esc="env.someKey"/></div>`);
(<any>env).someKey = "hey";
const widget = new Parent(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
await widget.updateEnv(<any>{ someKey: "rerendered" });
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>rerendered</div></div>");
});
});
describe("widget and observable state", () => {
test("widget is rerendered when its state is changed", async () => {
env.qweb.addTemplate("TestWidget", `<div><t t-esc="state.drink"/></div>`);
@@ -2961,71 +2839,6 @@ describe("widget and observable state", () => {
});
});
describe("t-mounted directive", () => {
test("callback is not called when not in DOM", async () => {
env.qweb.addTemplate("TestWidget", `<div><input t-mounted="f"/></div>`);
class TestWidget extends Widget {
f() {}
}
const widget = new TestWidget(env);
widget.f = jest.fn();
await widget.mount(document.createElement("div"));
expect(widget.f).toHaveBeenCalledTimes(0);
});
test("callback is called when in DOM", async () => {
env.qweb.addTemplate("TestWidget", `<div><input t-mounted="f"/></div>`);
class TestWidget extends Widget {
f() {}
}
const widget = new TestWidget(env);
widget.f = jest.fn();
await widget.mount(fixture);
expect(widget.f).toHaveBeenCalledTimes(1);
});
test("callback with args is called when in DOM", async () => {
env.qweb.addTemplate("TestWidget", `<div><input t-mounted="f(2)"/></div>`);
class TestWidget extends Widget {
f() {}
}
const widget = new TestWidget(env);
widget.f = jest.fn();
await widget.mount(fixture);
expect(widget.f).toHaveBeenCalledTimes(1);
expect(widget.f).toHaveBeenCalledWith(2);
});
test("combined with a t-if", async () => {
env.qweb.addTemplate("TestWidget", `<div><input t-if="state.flag" t-mounted="f"/></div>`);
class TestWidget extends Widget {
state = useState({ flag: false });
f() {}
}
const widget = new TestWidget(env);
widget.f = jest.fn();
await widget.mount(fixture);
expect(widget.f).toHaveBeenCalledTimes(0);
widget.state.flag = true;
await nextTick();
expect(widget.f).toHaveBeenCalledTimes(1);
});
test("combined with a t-ref", async () => {
env.qweb.addTemplate("TestWidget", `<div><input t-ref="input" t-mounted="f"/></div>`);
class TestWidget extends Widget {
input = useRef("input");
f() {}
}
const widget = new TestWidget(env);
widget.f = jest.fn();
await widget.mount(fixture);
expect(widget.input.el).toBeDefined();
expect(widget.f).toHaveBeenCalledTimes(1);
});
});
describe("can deduce template from name", () => {
test("can find template if name of component", async () => {
class ABC extends Widget {}
@@ -3509,6 +3322,34 @@ describe("t-slot directive", () => {
expect(fixture.innerHTML).toBe(`<a href="abc">hey</a>`);
});
test("template can just return a slot", async () => {
class Child extends Widget {
static template = xml`<span><t t-esc="props.value"/></span>`;
}
class SlotComponent extends Widget {
static template = xml`<t t-slot="default"/>`;
}
class Parent extends Widget {
static template = xml`
<div>
<SlotComponent><Child value="state.value"/></SlotComponent>
</div>`;
static components = { SlotComponent, Child };
state = useState({ value: 3 });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
expect(QWeb.TEMPLATES[SlotComponent.template].fn.toString()).toMatchSnapshot();
parent.state.value = 5;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>5</span></div>");
});
});
describe("t-model directive", () => {
@@ -4480,3 +4321,39 @@ describe("support svg components", () => {
);
});
});
describe("t-raw in components", () => {
test("update properly on state changes", async () => {
class TestW extends Widget {
static template = xml`<div><t t-raw="state.value"/></div>`;
state = useState({ value: "<b>content</b>" });
}
const widget = new TestW(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><b>content</b></div>");
widget.state.value = "<span>other content</span>";
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>other content</span></div>");
});
test("can render list of t-raw ", async () => {
class TestW extends Widget {
static template = xml`
<div>
<t t-foreach="state.items" t-as="item">
<t t-esc="item"/>
<t t-raw="item"/>
</t>
</div>`;
state = useState({ items: ["<b>one</b>", "<b>two</b>", "<b>tree</b>"] });
}
const widget = new TestW(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div>&lt;b&gt;one&lt;/b&gt;<b>one</b>&lt;b&gt;two&lt;/b&gt;<b>two</b>&lt;b&gt;tree&lt;/b&gt;<b>tree</b></div>"
);
});
});
+12
View File
@@ -1,6 +1,7 @@
import { Component, Env } from "../../src/component/component";
import { makeTestFixture, makeTestEnv } from "../helpers";
import { QWeb } from "../../src/qweb";
import { xml } from "../../src/tags";
//------------------------------------------------------------------------------
// Setup and helpers
@@ -31,6 +32,7 @@ describe("props validation", () => {
test("validation is only done in dev mode", async () => {
class TestWidget extends Widget {
static props = ["message"];
static template = xml`<div>hey</div>`;
}
QWeb.dev = true;
@@ -48,6 +50,7 @@ describe("props validation", () => {
test("props: list of strings", async () => {
class TestWidget extends Widget {
static props = ["message"];
static template = xml`<div>hey</div>`;
}
expect(() => {
@@ -67,6 +70,7 @@ describe("props validation", () => {
for (let test of Tests) {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = { p: test.type };
};
@@ -96,6 +100,7 @@ describe("props validation", () => {
for (let test of Tests) {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = { p: { type: test.type } };
};
@@ -115,6 +120,7 @@ describe("props validation", () => {
test("can validate a prop with multiple types", async () => {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = { p: [String, Boolean] };
};
@@ -130,6 +136,7 @@ describe("props validation", () => {
test("can validate an optional props", async () => {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = { p: { type: String, optional: true } };
};
@@ -145,6 +152,7 @@ describe("props validation", () => {
test("can validate an array with given primitive type", async () => {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = { p: { type: Array, element: String } };
};
@@ -164,6 +172,7 @@ describe("props validation", () => {
test("can validate an array with multiple sub element types", async () => {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = { p: { type: Array, element: [String, Boolean] } };
};
@@ -180,6 +189,7 @@ describe("props validation", () => {
test("can validate an object with simple shape", async () => {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = {
p: { type: Object, shape: { id: Number, url: String } }
};
@@ -201,6 +211,7 @@ describe("props validation", () => {
test("can validate recursively complicated prop def", async () => {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = {
p: {
type: Object,
@@ -316,6 +327,7 @@ describe("default props", () => {
test("can set default values", async () => {
class TestWidget extends Widget {
static defaultProps = { p: 4 };
static template = xml`<div>hey</div>`;
}
const w = new TestWidget(env, {});
+176
View File
@@ -0,0 +1,176 @@
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
import { Component, Env } from "../src/component/component";
import { Context, useContext } from "../src/context";
import { xml } from "../src/tags";
import { useState } from "../src/hooks";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("Context", () => {
test("very simple use, with initial value", async () => {
const testContext = new Context({ value: 123 });
class Test extends Component<any, any> {
static template = xml`<div><t t-esc="contextObj.value"/></div>`;
contextObj = useContext(testContext);
}
const test = new Test(env);
await test.mount(fixture);
expect(fixture.innerHTML).toBe("<div>123</div>");
});
test("useContext hook is reactive, for one component", async () => {
const testContext = new Context({ value: 123 });
class Test extends Component<any, any> {
static template = xml`<div><t t-esc="contextObj.value"/></div>`;
contextObj = useContext(testContext);
}
const test = new Test(env);
await test.mount(fixture);
expect(fixture.innerHTML).toBe("<div>123</div>");
test.contextObj.value = 321;
await nextTick();
expect(fixture.innerHTML).toBe("<div>321</div>");
});
test("two components can subscribe to same context", async () => {
const testContext = new Context({ value: 123 });
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="contextObj.value"/></span>`;
contextObj = useContext(testContext);
}
class Parent extends Component<any, any> {
static template = xml`<div><Child /><Child /></div>`;
static components = { Child };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
testContext.state.value = 321;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
});
test("one components can subscribe twice to same context", async () => {
const testContext = new Context({ a: 1, b: 2 });
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="contextObj1.a"/><t t-esc="contextObj2.b"/></span>`;
contextObj1 = useContext(testContext);
contextObj2 = useContext(testContext);
__render(fiber) {
steps.push("child");
return super.__render(fiber);
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child /></div>`;
static components = { Child };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
expect(steps).toEqual(["child"]);
testContext.state.a = 3;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>32</span></div>");
expect(steps).toEqual(["child", "child"]);
});
test("parent and children subscribed to same context", async () => {
const testContext = new Context({ a: 123, b: 321 });
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
contextObj = useContext(testContext);
__render(fiber) {
steps.push("child");
return super.__render(fiber);
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child /><t t-esc="contextObj.b"/></div>`;
static components = { Child };
contextObj = useContext(testContext);
__render(fiber) {
steps.push("parent");
return super.__render(fiber);
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span>321</div>");
expect(steps).toEqual(["parent", "child"]);
parent.contextObj.a = 124;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>124</span>321</div>");
// we only want one render from the child here, not two
expect(steps).toEqual(["parent", "child", "parent", "child"]);
});
test("destroyed component is inactive", async () => {
const testContext = new Context({ a: 123 });
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
contextObj = useContext(testContext);
__render(fiber) {
steps.push("child");
return super.__render(fiber);
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child t-if="state.flag"/></div>`;
static components = { Child };
state = useState({ flag: true });
__render(fiber) {
steps.push("parent");
return super.__render(fiber);
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
expect(steps).toEqual(["parent", "child"]);
expect(testContext.subscriptions.update.length).toBe(1);
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["parent", "child", "parent"]);
// kind of whitebox...
// we make sure we do not have any pending subscriptions to the 'update'
// event
expect(testContext.subscriptions.update.length).toBe(0);
});
});
+342 -1
View File
@@ -1,6 +1,16 @@
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
import { Component, Env } from "../src/component/component";
import { useState, onMounted, onWillUnmount, useRef } from "../src/hooks";
import {
useState,
onMounted,
onWillUnmount,
useRef,
onPatched,
onWillPatch,
onWillStart,
onWillUpdateProps,
useSubEnv
} from "../src/hooks";
import { xml } from "../src/tags";
//------------------------------------------------------------------------------
@@ -70,6 +80,40 @@ describe("hooks", () => {
expect(steps).toEqual(["mounted", "willunmount"]);
});
test("can use onMounted, onWillUnmount, part 2", async () => {
const steps: string[] = [];
function useMyHook() {
onMounted(() => {
steps.push("mounted");
});
onWillUnmount(() => {
steps.push("willunmount");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook();
}
}
class Parent extends Component<any, any> {
static template = xml`<div><MyComponent t-if="state.flag"/></div>`;
static components = { MyComponent };
state = useState({ flag: true });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
expect(steps).toEqual(["mounted"]);
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["mounted", "willunmount"]);
});
test("mounted, willUnmount, onMounted, onWillUnmount order", async () => {
const steps: string[] = [];
function useMyHook() {
@@ -101,6 +145,46 @@ describe("hooks", () => {
expect(steps).toEqual(["comp:mounted", "hook:mounted", "hook:willunmount", "comp:willunmount"]);
});
test("mounted, willUnmount, onMounted, onWillUnmount order, part 2", async () => {
const steps: string[] = [];
function useMyHook() {
onMounted(() => {
steps.push("hook:mounted");
});
onWillUnmount(() => {
steps.push("hook:willunmount");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook();
}
mounted() {
steps.push("comp:mounted");
}
willUnmount() {
steps.push("comp:willunmount");
}
}
class Parent extends Component<any, any> {
static template = xml`<div><MyComponent t-if="state.flag"/></div>`;
static components = { MyComponent };
state = useState({ flag: true });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["comp:mounted", "hook:mounted", "hook:willunmount", "comp:willunmount"]);
});
test("two different call to mounted/willunmount should work", async () => {
const steps: string[] = [];
function useMyHook(i) {
@@ -149,4 +233,261 @@ describe("hooks", () => {
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
});
test("useRef hook is null if ref is removed ", async () => {
expect.assertions(4);
class TestRef extends Component<any, any> {
static template = xml`<div><span t-if="state.flag" t-ref="span">owl</span></div>`;
spanRef = useRef("span");
state = useState({ flag: true });
willPatch() {
expect(this.spanRef.el).not.toBeNull();
}
patched() {
expect(this.spanRef.el).toBeNull();
}
}
const component = new TestRef(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>owl</span></div>");
component.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
});
test("can use onPatched, onWillPatch", async () => {
const steps: string[] = [];
function useMyHook() {
onWillPatch(() => {
steps.push("willPatch");
});
onPatched(() => {
steps.push("patched");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
state = useState({ flag: true });
constructor(env) {
super(env);
useMyHook();
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(component).not.toHaveProperty("patched");
expect(component).not.toHaveProperty("willPatch");
expect(steps).toEqual([]);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["willPatch", "patched"]);
});
test("patched, willPatch, onPatched, onWillPatch order", async () => {
const steps: string[] = [];
function useMyHook() {
onPatched(() => {
steps.push("hook:patched");
});
onWillPatch(() => {
steps.push("hook:willPatch");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
state = useState({ flag: true });
constructor(env) {
super(env);
useMyHook();
}
willPatch() {
steps.push("comp:willPatch");
}
patched() {
steps.push("comp:patched");
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.state.flag = false;
await nextTick();
expect(steps).toEqual(["hook:willPatch", "comp:willPatch", "comp:patched", "hook:patched"]);
});
test("two different call to willPatch/patched should work", async () => {
const steps: string[] = [];
function useMyHook(i) {
onPatched(() => {
steps.push("hook:patched" + i);
});
onWillPatch(() => {
steps.push("hook:willPatch" + i);
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey<t t-esc="state.value"/></div>`;
state = useState({ value: 1 });
constructor(env) {
super(env);
useMyHook(1);
useMyHook(2);
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey1</div>");
component.state.value++;
await nextTick();
expect(fixture.innerHTML).toBe("<div>hey2</div>");
expect(steps).toEqual(["hook:willPatch2", "hook:willPatch1", "hook:patched1", "hook:patched2"]);
});
describe("autofocus hook", () => {
function useAutofocus(name) {
let ref = useRef(name);
let isInDom = false;
function updateFocus() {
if (!isInDom && ref.el) {
isInDom = true;
ref.el.focus();
} else if (isInDom && !ref.el) {
isInDom = false;
}
}
onPatched(updateFocus);
onMounted(updateFocus);
}
test("simple input", async () => {
class SomeComponent extends Component<any, any> {
static template = xml`
<div>
<input t-ref="input1"/>
<input t-ref="input2"/>
</div>`;
constructor(env) {
super(env);
useAutofocus("input2");
}
}
const component = new SomeComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input><input></div>");
const input2 = fixture.querySelectorAll("input")[1];
expect(input2).toBe(document.activeElement);
});
test("input in a t-if", async () => {
class SomeComponent extends Component<any, any> {
static template = xml`
<div>
<input t-ref="input1"/>
<t t-if="state.flag"><input t-ref="input2"/></t>
</div>`;
state = useState({ flag: false });
constructor(env) {
super(env);
useAutofocus("input2");
}
}
const component = new SomeComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input></div>");
expect(document.activeElement).toBe(document.body);
component.state.flag = true;
await nextTick();
const input2 = fixture.querySelectorAll("input")[1];
expect(input2).toBe(document.activeElement);
});
});
test("can use sub env", async () => {
class TestComponent extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor(env) {
super(env);
useSubEnv({ val: 3 });
}
}
const component = new TestComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(env).not.toHaveProperty("val");
expect(component.env).toHaveProperty("val");
});
test("parent and child env", async () => {
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor(env) {
super(env);
useSubEnv({ val: 5 });
}
}
class Parent extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/><Child/></div>`;
static components = { Child };
constructor(env) {
super(env);
useSubEnv({ val: 3 });
}
}
const component = new Parent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3<div>5</div></div>");
});
test("can use onWillStart, onWillUpdateProps", async () => {
const steps: string[] = [];
function useMyHook() {
onWillStart(() => {
steps.push("onWillStart");
});
onWillUpdateProps(nextProps => {
expect(nextProps).toEqual({ value: 2 });
steps.push("onWillUpdateProps");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<span><t t-esc="props.value"/></span>`;
constructor(parent, props) {
super(parent, props);
useMyHook();
}
}
class App extends Component<any, any> {
static template = xml`<div><MyComponent value="state.value"/></div>`;
static components = { MyComponent };
state = useState({ value: 1 });
}
const app = new App(env);
await app.mount(fixture);
expect(app).not.toHaveProperty("willStart");
expect(app).not.toHaveProperty("willUpdateProps");
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
expect(steps).toEqual(["onWillStart"]);
app.state.value = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>2</span></div>");
expect(steps).toEqual(["onWillStart", "onWillUpdateProps"]);
});
});
+117 -141
View File
@@ -7,7 +7,6 @@ exports[`attributes class and t-attf-class with ternary operation 1`] = `
var _1 = 'hello ' + (context['value']?'world':'');
let c2 = [], p2 = {key:2,attrs:{class: _1}};
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -19,7 +18,6 @@ exports[`attributes dynamic attribute falsy variable 1`] = `
var _1 = context['value'];
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -31,7 +29,6 @@ exports[`attributes dynamic attribute with a dash 1`] = `
var _1 = context['id'];
let c2 = [], p2 = {key:2,attrs:{\\"data-action-id\\": _1}};
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -43,7 +40,6 @@ exports[`attributes dynamic attributes 1`] = `
var _1 = 'bar';
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -55,7 +51,6 @@ exports[`attributes dynamic formatted attributes with a dash 1`] = `
var _1 = \`Some text \${context['id']}\`;
let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -67,7 +62,6 @@ exports[`attributes fixed variable 1`] = `
var _1 = context['value'];
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -79,7 +73,6 @@ exports[`attributes format expression 1`] = `
var _1 = (context['value']+37);
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -91,7 +84,6 @@ exports[`attributes format expression, other format 1`] = `
var _1 = (context['value']+37);
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -103,7 +95,6 @@ exports[`attributes format literal 1`] = `
var _1 = \`bar\`;
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -115,7 +106,6 @@ exports[`attributes format multiple 1`] = `
var _1 = \`a \${context['value1']} is \${context['value2']} of \${context['value3']} ]\`;
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -127,7 +117,6 @@ exports[`attributes format value 1`] = `
var _1 = \`b\${context['value']}r\`;
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -139,7 +128,6 @@ exports[`attributes from object variables set previously 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = {a:'b'};
let _3 = utils.toObj(_2.a);
let c4 = [], p4 = {key:4,class:_3};
@@ -156,7 +144,6 @@ exports[`attributes from variables set previously 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = 'def';
let _3 = utils.toObj(_2);
let c4 = [], p4 = {key:4,class:_3};
@@ -180,7 +167,6 @@ exports[`attributes object 1`] = `
}
}
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -194,7 +180,6 @@ exports[`attributes static attributes 1`] = `
var _3 = 'c';
let c4 = [], p4 = {key:4,attrs:{foo: _1,bar: _2,baz: _3}};
var vn4 = h('div', p4, c4);
result = vn4;
return vn4;
}"
`;
@@ -207,7 +192,6 @@ exports[`attributes static attributes on void elements 1`] = `
var _2 = 'Test';
let c3 = [], p3 = {key:3,attrs:{src: _1,alt: _2}};
var vn3 = h('img', p3, c3);
result = vn3;
return vn3;
}"
`;
@@ -219,7 +203,6 @@ exports[`attributes static attributes with dashes 1`] = `
var _1 = 'Close';
let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -233,7 +216,6 @@ exports[`attributes t-att-class and class should combine together 1`] = `
Object.assign(_2, utils.toObj(context['value']))
let c3 = [], p3 = {key:3,class:_2};
var vn3 = h('div', p3, c3);
result = vn3;
return vn3;
}"
`;
@@ -247,7 +229,6 @@ exports[`attributes t-att-class with object 1`] = `
Object.assign(_2, utils.toObj({a:context['b'],c:context['d'],e:context['f']}))
let c3 = [], p3 = {key:3,class:_2};
var vn3 = h('div', p3, c3);
result = vn3;
return vn3;
}"
`;
@@ -259,7 +240,6 @@ exports[`attributes t-attf-class should combine with class 1`] = `
var _1 = 'hello ' + \`world\`;
let c2 = [], p2 = {key:2,attrs:{class: _1}};
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -278,7 +258,6 @@ exports[`attributes tuple literal 1`] = `
}
}
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -297,7 +276,6 @@ exports[`attributes tuple variable 1`] = `
}
}
var vn2 = h('div', p2, c2);
result = vn2;
return vn2;
}"
`;
@@ -309,7 +287,6 @@ exports[`debugging t-debug 1`] = `
debugger;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
if (true) {
debugger;
let c2 = [], p2 = {key:2};
@@ -327,7 +304,6 @@ exports[`debugging t-log 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = 42;
console.log(_2+3)
return vn1;
@@ -341,7 +317,6 @@ exports[`foreach does not pollute the rendering context 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = [1];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2;
@@ -372,7 +347,6 @@ exports[`foreach iterate on items (on a element node) 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = [1,2];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2;
@@ -407,7 +381,6 @@ exports[`foreach iterate on items 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = [3,2,1];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2;
@@ -450,7 +423,6 @@ exports[`foreach iterate, dict param 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = context['value'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2;
@@ -493,7 +465,6 @@ exports[`foreach iterate, position 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = Array(5);
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2;
@@ -533,7 +504,6 @@ exports[`foreach warn if no key in some case 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = [1,2];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2;
@@ -566,7 +536,6 @@ exports[`loading templates can initialize qweb with a string 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
c1.push({text: \`jupiler\`});
return vn1;
}"
@@ -578,7 +547,6 @@ exports[`loading templates can load a few templates from a xml string 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('ul', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2};
var vn2 = h('li', p2, c2);
c1.push(vn2);
@@ -599,7 +567,6 @@ exports[`misc global 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = [4,5,6];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2;
@@ -614,7 +581,7 @@ exports[`misc global 1`] = `
context.value_index = i;
context.value = _3[i];
context.value_value = _4[i];
let c5 = [], p5 = {key:5,on:{}};
let c5 = [], p5 = {key:5};
var vn5 = h('span', p5, c5);
c1.push(vn5);
var _6 = context['value'];
@@ -624,12 +591,12 @@ exports[`misc global 1`] = `
{
let _11 = 'bbb';
var _13 = 'agüero';
let c14 = [], p14 = {key:14,attrs:{\\"falló\\": _13},on:{}};
var vn14 = h('Año', p14, c14);
let c14 = [], p14 = {key:14,attrs:{\\"falló\\": _13}};
var vn14 = h('año', p14, c14);
c1.push(vn14);
{
let _15 = 'aaa';
let c16 = [], p16 = {key:16,on:{}};
let c16 = [], p16 = {key:16};
var vn16 = h('span', p16, c16);
c14.push(vn16);
if (_15 || _15 === 0) {
@@ -638,7 +605,7 @@ exports[`misc global 1`] = `
c16.push({text: \`foo default\`});
}
}
let c17 = [], p17 = {key:17,on:{}};
let c17 = [], p17 = {key:17};
var vn17 = h('span', p17, c17);
c14.push(vn17);
var _18 = context['foo'];
@@ -648,7 +615,7 @@ exports[`misc global 1`] = `
c17.push({text: \`foo default\`});
}
var _19 = 'bbb';
let c20 = [], p20 = {key:20,on:{}};
let c20 = [], p20 = {key:20};
var vn20 = h('span', p20, c20);
c14.push(vn20);
if (_19 || _19 === 0) {
@@ -658,17 +625,12 @@ exports[`misc global 1`] = `
}
}
}
let c21 = [], p21 = {key:21,on:{}};
let c21 = [], p21 = {key:21};
var vn21 = h('div', p21, c21);
c1.push(vn21);
var _22 = context['toto'];
if (_22 || _22 === 0) {
var frag23 = utils.getFragment(_22)
var p24 = {hook: {
insert: n => n.elm.parentNode.replaceChild(frag23, n.elm),
}};
var vn24 = h('div', p24)
c21.push(vn24);
c21.push(...utils.htmlToVDOM(_22));
} else {
c21.push({text: \`toto default\`});
}
@@ -683,7 +645,6 @@ exports[`properly support svg add proper namespace to g tags 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('g', p1, c1);
result = vn1;
var _2 = '50';
var _3 = '50';
var _4 = '4';
@@ -708,7 +669,6 @@ exports[`properly support svg add proper namespace to svg 1`] = `
var _2 = '90px';
let c3 = [], p3 = {key:3,attrs:{width: _1,height: _2}};
var vn3 = h('svg', p3, c3);
result = vn3;
var _4 = '50';
var _5 = '50';
var _6 = '4';
@@ -732,7 +692,6 @@ exports[`special cases for some boolean html attributes/properties input type= c
var _2 = context['flag'];
let c3 = [], p3 = {key:3,attrs:{type: _1,checked: _2},props:{checked: _2}};
var vn3 = h('input', p3, c3);
result = vn3;
return vn3;
}"
`;
@@ -743,7 +702,6 @@ exports[`special cases for some boolean html attributes/properties various boole
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = 'checkbox';
var _3 = 'checked';
let c4 = [], p4 = {key:4,attrs:{type: _2,checked: _3},props:{checked: _3}};
@@ -784,7 +742,6 @@ exports[`static templates div with a span child node 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
@@ -799,7 +756,6 @@ exports[`static templates div with a text node 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
c1.push({text: \`word\`});
return vn1;
}"
@@ -811,19 +767,19 @@ exports[`static templates empty div 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
return vn1;
}"
`;
exports[`static templates ignore comments 1`] = `
exports[`static templates properly handle comments 1`] = `
"function anonymous(context,extra
) {
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
c1.push({text: \`hello owl\`});
c1.push({text: \`hello \`});
c1.push(h('!', \` comment\`));
c1.push({text: \`owl\`});
return vn1;
}"
`;
@@ -887,7 +843,6 @@ exports[`t-call (template calling call with several sub nodes on same line 1`] =
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c5 = [], p5 = {key:5};
var vn5 = h('div', p5, c5);
c1.push(vn5);
@@ -910,7 +865,6 @@ exports[`t-call (template calling inherit context 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = 1;
if (_2 || _2 === 0) {
c1.push({text: _2});
@@ -926,7 +880,6 @@ exports[`t-call (template calling recursive template, part 1 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
@@ -967,7 +920,6 @@ exports[`t-call (template calling recursive template, part 2 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
{
let _2 = context['root'];
let c3 = [], p3 = {key:3};
@@ -1059,7 +1011,6 @@ exports[`t-call (template calling recursive template, part 3 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
{
let _2 = context['root'];
let c3 = [], p3 = {key:3};
@@ -1148,7 +1099,6 @@ exports[`t-call (template calling scoped parameters 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
{
let _2 = 42;
c1.push({text: \`ok\`});
@@ -1167,7 +1117,6 @@ exports[`t-call (template calling t-call with t-if 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
if (context['flag']) {
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
@@ -1184,7 +1133,6 @@ exports[`t-call (template calling t-call, global templates 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
@@ -1241,7 +1189,6 @@ exports[`t-call (template calling with used set body 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
result = vn1;
{
let _2 = 'ok';
if (_2 || _2 === 0) {
@@ -1256,9 +1203,8 @@ exports[`t-esc escaping on a node 1`] = `
"function anonymous(context,extra
) {
var h = this.h;
let c1 = [], p1 = {key:1,on:{}};
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
result = vn1;
var _2 = 'ok';
if (_2 || _2 === 0) {
c1.push({text: _2});
@@ -1271,9 +1217,8 @@ exports[`t-esc escaping on a node with a body 1`] = `
"function anonymous(context,extra
) {
var h = this.h;
let c1 = [], p1 = {key:1,on:{}};
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
result = vn1;
var _2 = 'ok';
if (_2 || _2 === 0) {
c1.push({text: _2});
@@ -1288,9 +1233,8 @@ exports[`t-esc escaping on a node with a body, as a default 1`] = `
"function anonymous(context,extra
) {
var h = this.h;
let c1 = [], p1 = {key:1,on:{}};
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
result = vn1;
var _2 = context['var'];
if (_2 || _2 === 0) {
c1.push({text: _2});
@@ -1307,7 +1251,6 @@ exports[`t-esc literal 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
result = vn1;
var _2 = 'ok';
if (_2 || _2 === 0) {
c1.push({text: _2});
@@ -1322,7 +1265,6 @@ exports[`t-esc variable 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
result = vn1;
var _2 = context['var'];
if (_2 || _2 === 0) {
c1.push({text: _2});
@@ -1337,7 +1279,6 @@ exports[`t-if boolean value condition elif 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
if (context['color']=='black') {
c1.push({text: \`black pearl\`});
}
@@ -1360,7 +1301,6 @@ exports[`t-if boolean value condition else 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
@@ -1385,7 +1325,6 @@ exports[`t-if boolean value condition false else 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
@@ -1410,7 +1349,6 @@ exports[`t-if boolean value condition missing 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
result = vn1;
if (context['condition']) {
c1.push({text: \`fail\`});
}
@@ -1424,7 +1362,6 @@ exports[`t-if boolean value false condition 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
if (context['condition']) {
c1.push({text: \`ok\`});
}
@@ -1438,7 +1375,6 @@ exports[`t-if boolean value true condition 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
if (context['condition']) {
c1.push({text: \`ok\`});
}
@@ -1452,7 +1388,6 @@ exports[`t-if can use some boolean operators in expressions 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
if (context['cond1']&&context['cond2']) {
c1.push({text: \`and\`});
}
@@ -1488,7 +1423,6 @@ exports[`t-key can use t-key directive on a node 1`] = `
const nodeKey1 = context['beer'].id
let c1 = [], p1 = {key:nodeKey1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = context['beer'].name;
if (_2 || _2 === 0) {
c1.push({text: _2});
@@ -1504,7 +1438,6 @@ exports[`t-key t-key directive in a list 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('ul', p1, c1);
result = vn1;
var _2 = context['beers'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2;
@@ -1541,7 +1474,6 @@ exports[`t-on can bind event handler 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('button', p1, c1);
result = vn1;
if (!context['add']) {
throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)
}
@@ -1559,7 +1491,6 @@ exports[`t-on can bind handlers with arguments 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('button', p1, c1);
result = vn1;
if (!context['add']) {
throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)
}
@@ -1576,7 +1507,6 @@ exports[`t-on can bind handlers with empty object (with non empty inner string)
var h = this.h;
let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('button', p1, c1);
result = vn1;
if (!context['doSomething']) {
throw new Error('Missing handler \\\\'' + 'doSomething' + \`\\\\' when evaluating template 'test'\`)
}
@@ -1593,7 +1523,6 @@ exports[`t-on can bind handlers with empty object 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('button', p1, c1);
result = vn1;
if (!context['doSomething']) {
throw new Error('Missing handler \\\\'' + 'doSomething' + \`\\\\' when evaluating template 'test'\`)
}
@@ -1611,7 +1540,6 @@ exports[`t-on can bind handlers with loop variable as argument 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('ul', p1, c1);
result = vn1;
var _2 = ['someval'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2;
@@ -1650,7 +1578,6 @@ exports[`t-on can bind handlers with object arguments 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('button', p1, c1);
result = vn1;
if (!context['add']) {
throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)
}
@@ -1667,7 +1594,6 @@ exports[`t-on can bind two event handlers 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('button', p1, c1);
result = vn1;
if (!context['handleClick']) {
throw new Error('Missing handler \\\\'' + 'handleClick' + \`\\\\' when evaluating template 'test'\`)
}
@@ -1690,7 +1616,6 @@ exports[`t-on handler is bound to proper owner 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('button', p1, c1);
result = vn1;
if (!context['add']) {
throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)
}
@@ -1701,6 +1626,53 @@ exports[`t-on handler is bound to proper owner 1`] = `
}"
`;
exports[`t-on t-on combined with t-esc 1`] = `
"function anonymous(context,extra
) {
let owner = context;
var h = this.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['onClick']) {
throw new Error('Missing handler \\\\'' + 'onClick' + \`\\\\' when evaluating template 'test'\`)
}
extra.handlers['click' + 2] = extra.handlers['click' + 2] || context['onClick'].bind(owner);
p2.on['click'] = extra.handlers['click' + 2];
var _3 = context['text'];
if (_3 || _3 === 0) {
c2.push({text: _3});
}
return vn1;
}"
`;
exports[`t-on t-on combined with t-raw 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let owner = context;
var h = this.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['onClick']) {
throw new Error('Missing handler \\\\'' + 'onClick' + \`\\\\' when evaluating template 'test'\`)
}
extra.handlers['click' + 2] = extra.handlers['click' + 2] || context['onClick'].bind(owner);
p2.on['click'] = extra.handlers['click' + 2];
var _3 = context['html'];
if (_3 || _3 === 0) {
c2.push(...utils.htmlToVDOM(_3));
}
return vn1;
}"
`;
exports[`t-on t-on with prevent and self modifiers (order matters) 1`] = `
"function anonymous(context,extra
) {
@@ -1708,7 +1680,6 @@ exports[`t-on t-on with prevent and self modifiers (order matters) 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('button', p2, c2);
c1.push(vn2);
@@ -1732,7 +1703,6 @@ exports[`t-on t-on with prevent and/or stop modifiers 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('button', p2, c2);
c1.push(vn2);
@@ -1764,6 +1734,48 @@ exports[`t-on t-on with prevent and/or stop modifiers 1`] = `
}"
`;
exports[`t-on t-on with prevent modifier in t-foreach 1`] = `
"function anonymous(context,extra
) {
let owner = context;
context = Object.create(context);
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = context['projects'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
}
var _length3 = _3.length;
for (let i = 0; i < _length3; i++) {
context.project_first = i === 0;
context.project_last = i === _length3 - 1;
context.project_index = i;
context.project = _3[i];
context.project_value = _4[i];
var _5 = '#';
const nodeKey6 = context['project']
let c6 = [], p6 = {key:nodeKey6,attrs:{href: _5},on:{}};
var vn6 = h('a', p6, c6);
c1.push(vn6);
if (!context['onEdit']) {
throw new Error('Missing handler \\\\'' + 'onEdit' + \`\\\\' when evaluating template 'test'\`)
}
const handler7 = context['onEdit'].bind(owner, context['project'].id);
p6.on['click'] = function (e) {e.preventDefault();handler7(e);};
c6.push({text: \` Edit \`});
var _8 = context['project'].name;
if (_8 || _8 === 0) {
c6.push({text: _8});
}
}
return vn1;
}"
`;
exports[`t-on t-on with self and prevent modifiers (order matters) 1`] = `
"function anonymous(context,extra
) {
@@ -1771,7 +1783,6 @@ exports[`t-on t-on with self and prevent modifiers (order matters) 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('button', p2, c2);
c1.push(vn2);
@@ -1795,7 +1806,6 @@ exports[`t-on t-on with self modifier 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('button', p2, c2);
c1.push(vn2);
@@ -1831,15 +1841,9 @@ exports[`t-raw literal 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
result = vn1;
var _2 = 'ok';
if (_2 || _2 === 0) {
var frag3 = utils.getFragment(_2)
var p4 = {hook: {
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
}};
var vn4 = h('div', p4)
c1.push(vn4);
c1.push(...utils.htmlToVDOM(_2));
}
return vn1;
}"
@@ -1852,15 +1856,9 @@ exports[`t-raw not escaping 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = context['var'];
if (_2 || _2 === 0) {
var frag3 = utils.getFragment(_2)
var p4 = {hook: {
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
}};
var vn4 = h('div', p4)
c1.push(vn4);
c1.push(...utils.htmlToVDOM(_2));
}
return vn1;
}"
@@ -1873,19 +1871,13 @@ exports[`t-raw t-raw and another sibling node 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
c2.push({text: \`hello\`});
var _3 = context['var'];
if (_3 || _3 === 0) {
var frag4 = utils.getFragment(_3)
var p5 = {hook: {
insert: n => n.elm.parentNode.replaceChild(frag4, n.elm),
}};
var vn5 = h('div', p5)
c1.push(vn5);
c1.push(...utils.htmlToVDOM(_3));
}
return vn1;
}"
@@ -1898,15 +1890,9 @@ exports[`t-raw variable 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
result = vn1;
var _2 = context['var'];
if (_2 || _2 === 0) {
var frag3 = utils.getFragment(_2)
var p4 = {hook: {
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
}};
var vn4 = h('div', p4)
c1.push(vn4);
c1.push(...utils.htmlToVDOM(_2));
}
return vn1;
}"
@@ -1919,7 +1905,6 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
@@ -1928,6 +1913,9 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
create: (_, n) => {
context.__owl__.refs[ref3] = n.elm;
},
destroy: () => {
delete context.__owl__.refs[ref3];
},
};
return vn1;
}"
@@ -1940,7 +1928,6 @@ exports[`t-ref can get a ref on a node 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
@@ -1949,6 +1936,9 @@ exports[`t-ref can get a ref on a node 1`] = `
create: (_, n) => {
context.__owl__.refs[ref3] = n.elm;
},
destroy: () => {
delete context.__owl__.refs[ref3];
},
};
return vn1;
}"
@@ -1962,7 +1952,6 @@ exports[`t-ref refs in a loop 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = context['items'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2;
@@ -1986,6 +1975,9 @@ exports[`t-ref refs in a loop 1`] = `
create: (_, n) => {
context.__owl__.refs[ref6] = n.elm;
},
destroy: () => {
delete context.__owl__.refs[ref6];
},
};
var _7 = context['item'];
if (_7 || _7 === 0) {
@@ -2002,7 +1994,6 @@ exports[`t-set evaluate value expression 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = 1+2;
if (_2 || _2 === 0) {
c1.push({text: _2});
@@ -2017,7 +2008,6 @@ exports[`t-set evaluate value expression, part 2 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = context['somevariable']+2;
if (_2 || _2 === 0) {
c1.push({text: _2});
@@ -2032,7 +2022,6 @@ exports[`t-set set from attribute literal 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = 'ok';
if (_2 || _2 === 0) {
c1.push({text: _2});
@@ -2047,7 +2036,6 @@ exports[`t-set set from attribute lookup 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = context['value'];
if (_2 || _2 === 0) {
c1.push({text: _2});
@@ -2073,7 +2061,6 @@ exports[`t-set set from body lookup 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = context['value'];
if (_2 || _2 === 0) {
c1.push({text: _2});
@@ -2088,7 +2075,6 @@ exports[`t-set set from empty body 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
return vn1;
}"
`;
@@ -2099,7 +2085,6 @@ exports[`t-set t-set and t-if 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = context['value'];
if (_2==='ok') {
c1.push({text: \`grimbergen\`});
@@ -2114,7 +2099,6 @@ exports[`t-set t-set evaluates an expression only once 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = context['value']+' artois';
if (_2 || _2 === 0) {
c1.push({text: _2});
@@ -2133,7 +2117,6 @@ exports[`t-set t-set should reuse variable if possible 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = 1;
var _3 = context['list'];
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
@@ -2172,7 +2155,6 @@ exports[`t-set value priority 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
var _2 = 1;
if (_2 || _2 === 0) {
c1.push({text: _2});
@@ -2187,7 +2169,6 @@ exports[`whitespace handling consecutives whitespaces are condensed into a singl
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
c1.push({text: \` abc \`});
return vn1;
}"
@@ -2199,7 +2180,6 @@ exports[`whitespace handling nothing is done in pre tags 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('pre', p1, c1);
result = vn1;
c1.push({text: \` \`});
return vn1;
}"
@@ -2211,7 +2191,6 @@ exports[`whitespace handling nothing is done in pre tags 2`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('pre', p1, c1);
result = vn1;
c1.push({text: \`
some text
\`});
@@ -2225,7 +2204,6 @@ exports[`whitespace handling nothing is done in pre tags 3`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('pre', p1, c1);
result = vn1;
c1.push({text: \`
\`});
@@ -2239,7 +2217,6 @@ exports[`whitespace handling white space only text nodes are condensed into a si
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
c1.push({text: \` \`});
return vn1;
}"
@@ -2251,7 +2228,6 @@ exports[`whitespace handling whitespace only text nodes with newlines are remove
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
+75 -3
View File
@@ -50,9 +50,9 @@ describe("static templates", () => {
expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>");
});
test("ignore comments", () => {
test("properly handle comments", () => {
qweb.addTemplate("test", "<div>hello <!-- comment-->owl</div>");
expect(renderToString(qweb, "test")).toBe("<div>hello owl</div>");
expect(renderToString(qweb, "test")).toBe("<div>hello <!-- comment-->owl</div>");
});
});
@@ -778,7 +778,7 @@ describe("foreach", () => {
describe("misc", () => {
test("global", () => {
qweb.addTemplate("_callee-asc", `<Año t-att-falló="'agüero'" t-raw="0"/>`);
qweb.addTemplate("_callee-asc", `<año t-att-falló="'agüero'" t-raw="0"/>`);
qweb.addTemplate("_callee-uses-foo", `<span t-esc="foo">foo default</span>`);
qweb.addTemplate("_callee-asc-toto", `<div t-raw="toto">toto default</div>`);
qweb.addTemplate(
@@ -1086,6 +1086,78 @@ describe("t-on", () => {
expect(steps).toEqual([true, true]);
});
test("t-on with prevent modifier in t-foreach", async () => {
expect.assertions(5);
qweb.addTemplate(
"test",
`<div>
<t t-foreach="projects" t-as="project">
<a href="#" t-key="project" t-on-click.prevent="onEdit(project.id)">
Edit <t t-esc="project.name"/>
</a>
</t>
</div>`
);
const steps: string[] = [];
const owner = {
projects: [{ id: 1, name: "Project 1" }, { id: 2, name: "Project 2" }],
onEdit(projectId, ev) {
expect(ev.defaultPrevented).toBe(true);
steps.push(projectId);
}
};
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
expect(node.outerHTML).toBe(
`<div><a href="#"> Edit Project 1</a><a href="#"> Edit Project 2</a></div>`
);
const links = node.querySelectorAll("a")!;
links[0].click();
links[1].click();
expect(steps).toEqual([1, 2]);
});
test("t-on combined with t-esc", async () => {
expect.assertions(3);
qweb.addTemplate("test", `<div><button t-on-click="onClick" t-esc="text"/></div>`);
const steps: string[] = [];
const owner = {
text: "Click here",
onClick() {
steps.push("onClick");
}
};
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
expect(node.outerHTML).toBe(`<div><button>Click here</button></div>`);
node.querySelector("button")!.click();
expect(steps).toEqual(["onClick"]);
});
test("t-on combined with t-raw", async () => {
expect.assertions(3);
qweb.addTemplate("test", `<div><button t-on-click="onClick" t-raw="html"/></div>`);
const steps: string[] = [];
const owner = {
html: "Click <b>here</b>",
onClick() {
steps.push("onClick");
}
};
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
expect(node.outerHTML).toBe(`<div><button>Click <b>here</b></button></div>`);
node.querySelector("button")!.click();
expect(steps).toEqual(["onClick"]);
});
});
describe("t-ref", () => {
@@ -10,7 +10,6 @@ exports[`Link component can render simple cases 1`] = `
var _2 = context['href'];
let c3 = [], p3 = {key:3,attrs:{href: _2},class:_1,on:{}};
var vn3 = h('a', p3, c3);
result = vn3;
if (!context['navigate']) {
throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__template__1'\`)
}
@@ -1,8 +1,8 @@
import { Component } from "../../src/component/component";
import { Link } from "../../src/router/Link";
import { RouterEnv } from "../../src/router/Router";
import { Link } from "../../src/router/link";
import { RouterEnv } from "../../src/router/router";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
import { TestRouter } from "./TestRouter";
import { TestRouter } from "./test_router";
describe("Link component", () => {
let fixture: HTMLElement;
@@ -1,8 +1,8 @@
import { Component } from "../../src/component/component";
import { RouterEnv } from "../../src/router/Router";
import { RouteComponent } from "../../src/router/RouteComponent";
import { RouterEnv } from "../../src/router/router";
import { RouteComponent } from "../../src/router/route_component";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
import { TestRouter } from "./TestRouter";
import { TestRouter } from "./test_router";
describe("RouteComponent", () => {
let fixture: HTMLElement;
+2 -2
View File
@@ -1,6 +1,6 @@
import { Destination, RouterEnv, Route } from "../../src/router/Router";
import { Destination, RouterEnv, Route } from "../../src/router/router";
import { makeTestEnv, nextTick } from "../helpers";
import { TestRouter } from "./TestRouter";
import { TestRouter } from "./test_router";
let env: RouterEnv;
let router: TestRouter | null = null;
@@ -1,4 +1,4 @@
import { Router } from "../../src/router/Router";
import { Router } from "../../src/router/router";
import { QWeb } from "../../src/qweb/index";
export class TestRouter extends Router {
@@ -1,6 +1,6 @@
import { Env } from "../../src/component/component";
import { Store, Getter } from "../../src/store/store";
import { nextTick, nextMicroTick } from "../helpers";
import { Env } from "../src/component/component";
import { Store, Getter } from "../src/store";
import { nextTick, nextMicroTick } from "./helpers";
describe("basic use", () => {
test("dispatch an action", () => {
@@ -1,21 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`connecting a component to store connecting a component to a local store 1`] = `"<div></div>"`;
exports[`connecting a component to store connecting a component to a local store 2`] = `"<div><span>hello</span></div>"`;
exports[`connecting a component to store connecting a component works 1`] = `"<div></div>"`;
exports[`connecting a component to store connecting a component works 2`] = `"<div><span>hello</span></div>"`;
exports[`connecting a component to store deep and shallow connecting a component 1`] = `"<div><span>Kasteel</span></div>"`;
exports[`connecting a component to store deep and shallow connecting a component 2`] = `"<div><span>Kasteel</span></div>"`;
exports[`connecting a component to store deep and shallow connecting a component 3`] = `"<div><span>Bertinchamps</span></div>"`;
exports[`connecting a component to store deep and shallow connecting a component 4`] = `"<div><span>Kasteel</span></div>"`;
exports[`various scenarios scenarios with async store updates and some components events 1`] = `"<div><button>Do stuff</button><div><span>Attachment 100</span><span>Name: text.txt</span></div></div>"`;
exports[`various scenarios scenarios with async store updates and some components events 2`] = `"<div><button>Do stuff</button></div>"`;
File diff suppressed because it is too large Load Diff
+990
View File
@@ -0,0 +1,990 @@
import { Component, Env } from "../src/component/component";
import { Store, useStore, useDispatch, useGetters } from "../src/store";
import { useState } from "../src/hooks";
import { xml } from "../src/tags";
import { shallowEqual } from "../src/utils";
import { makeTestEnv, makeTestFixture, nextTick, makeDeferred } from "./helpers";
describe("connecting a component to store", () => {
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
test("connecting a component works, with useStore", async () => {
let nextId = 1;
const state = { todos: [] };
const actions = {
addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ });
}
};
const store = new Store({ state, actions });
class App extends Component<any, any> {
static template = xml`
<div>
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
</div>`;
todos = useStore(state => state.todos);
}
(<any>env).store = store;
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>");
await store.dispatch("addTodo", "hello");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>hello</span></div>");
});
test("can use useStore twice in a component", async () => {
const state = { a: 1, b: 2 };
const actions = {
doSomething({ state }) {
state.a = 2;
state.b = 3;
}
};
const store = new Store({ state, actions });
class App extends Component<any, any> {
static template = xml`
<div>
<span t-esc="a.value"/>
<span t-esc="b.value"/>
</div>`;
a = useStore(state => ({value: state.a}));
b = useStore(state => ({value: state.b}));
}
App.prototype.__render = jest.fn(App.prototype.__render);
(<any>env).store = store;
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>1</span><span>2</span></div>");
expect(App.prototype.__render).toBeCalledTimes(1);
await store.dispatch("doSomething");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>2</span><span>3</span></div>");
expect(App.prototype.__render).toBeCalledTimes(2);
});
test("useStore: do not re-render if not changed", async () => {
let nextId = 1;
const state = { todos: [], a: 1 };
const actions = {
addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ });
}
};
const store = new Store({ state, actions });
class App extends Component<any, any> {
static template = xml`
<div>
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
</div>`;
todos = useStore(state => state.todos);
}
App.prototype.__render = jest.fn(App.prototype.__render);
(<any>env).store = store;
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>");
expect(App.prototype.__render).toBeCalledTimes(1);
store.state.todos.push({ id: 3, msg: "hello" });
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>hello</span></div>");
expect(App.prototype.__render).toBeCalledTimes(2);
store.state.a = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>hello</span></div>");
expect(App.prototype.__render).toBeCalledTimes(2);
});
test("connecting a component with useStore returning number", async () => {
let nextId = 1;
const state = { todos: [] };
const actions = {
addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ });
}
};
const store = new Store({ state, actions });
class App extends Component<any, any> {
static template = xml`<div><t t-esc="nbrTodos.value"/></div>`;
nbrTodos = useStore(state => ({ value: state.todos.length }));
}
(<any>env).store = store;
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>0</div>");
await store.dispatch("addTodo", "hello");
await nextTick();
expect(fixture.innerHTML).toBe("<div>1</div>");
});
test("connecting a component with useStore returning number", async () => {
let nextId = 1;
const state = { todos: [], a: 1 };
const actions = {
addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ });
},
incrementA({ state }) {
state.a++;
}
};
const store = new Store({ state, actions });
class App extends Component<any, any> {
static template = xml`<div><t t-esc="nbrTodos.value"/></div>`;
nbrTodos = useStore(state => ({ value: state.todos.length }), { isEqual: shallowEqual });
}
App.prototype.__render = jest.fn(App.prototype.__render);
(<any>env).store = store;
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>0</div>");
expect(App.prototype.__render).toBeCalledTimes(1);
await store.dispatch("addTodo", "hello");
await nextTick();
expect(fixture.innerHTML).toBe("<div>1</div>");
expect(App.prototype.__render).toBeCalledTimes(2);
await store.dispatch("incrementA");
await nextTick();
expect(fixture.innerHTML).toBe("<div>1</div>");
expect(App.prototype.__render).toBeCalledTimes(2);
});
test("connecting a component to a local store", async () => {
let nextId = 1;
const state = { todos: [] };
const actions = {
addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ });
}
};
const store = new Store({ state, actions });
class App extends Component<any, any> {
static template = xml`
<div>
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
</div>`;
todos = useStore(state => state.todos, { store });
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>");
await store.dispatch("addTodo", "hello");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>hello</span></div>");
});
test("can dispatch actions from a connected component", async () => {
const store = new Store({
state: { value: 1 },
actions: {
inc({ state }) {
state.value++;
}
}
});
(<any>env).store = store;
class App extends Component<any, any> {
static template = xml`
<div>
<button t-on-click="dispatch('inc')">Inc</button>
<span><t t-esc="storeState.value"/></span>
</div>`;
storeState = useStore(state => state);
dispatch = useDispatch();
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>Inc</button><span>1</span></div>");
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>Inc</button><span>2</span></div>");
});
test("useStore can use props", async () => {
const state = { todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "chimay" }] };
const store = new Store({ state, actions: {} });
class TodoItem extends Component<any, any> {
static template = xml`<span><t t-esc="todo.text"/></span>`;
todo = useStore((state, props) => {
return state.todos.find(t => t.id === props.todoId);
});
}
class App extends Component<any, any> {
static template = xml`<div><TodoItem todoId="state.currentId"/></div>`;
static components = { TodoItem };
state = useState({ currentId: 1 });
}
(<any>env).store = store;
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
app.state.currentId = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>chimay</span></div>");
});
test("useStore receives props as second argument", async () => {
const state = { todos: [{ id: 1, text: "jupiler" }] };
let nextId = 2;
const actions = {
addTodo({ state }, text) {
state.todos.push({ text, id: nextId++ });
}
};
const store = new Store({ state, actions });
class TodoItem extends Component<any, any> {
static template = xml`<span><t t-esc="todo.text"/></span>`;
todo = useStore((state, props) => {
return state.todos.find(t => t.id === props.id);
});
}
class TodoList extends Component<any, any> {
static template = xml`
<div>
<TodoItem t-foreach="todos" t-as="todo" id="todo.id" t-key="todo.id"/>
</div>`;
static components = { TodoItem };
todos = useStore(state => state.todos);
}
(<any>env).store = store;
const app = new TodoList(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
store.dispatch("addTodo", "hoegaarden");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>hoegaarden</span></div>");
});
test("can call useGetters to receive store getters", async () => {
const state = {
importantID: 1,
todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "bertinchamps" }]
};
const getters = {
importantTodoText({ state }) {
return state.todos.find(todo => todo.id === state.importantID).text;
},
text({ state }, id) {
return state.todos.find(todo => todo.id === id).text;
}
};
const store = new Store({ state, getters });
class TodoItem extends Component<any, any> {
static template = xml`
<div>
<span><t t-esc="storeProps.activeTodoText"/></span>
<span><t t-esc="storeProps.importantTodoText"/></span>
</div>`;
getters = useGetters();
storeProps = useStore((state, props) => {
const todo = state.todos.find(t => t.id === props.id);
return {
activeTodoText: this.getters.text(todo.id),
importantTodoText: this.getters.importantTodoText()
};
});
}
class TodoList extends Component<any, any> {
static components = { TodoItem };
static template = xml`
<div>
<t t-foreach="todos" t-as="todo">
<TodoItem id="todo.id" t-key="todo.id"/>
</t>
</div>`;
todos = useStore(state => state.todos);
}
(<any>env).store = store;
const app = new TodoList(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div><span>jupiler</span><span>jupiler</span></div><div><span>bertinchamps</span><span>jupiler</span></div></div>"
);
});
test("connected component is updated when props are updated", async () => {
class Beer extends Component<any, any> {
static template = xml`<span><t t-esc="beer.name"/></span>`;
beer = useStore((state, props) => state.beers[props.id]);
}
class App extends Component<any, any> {
static template = xml`<div><Beer id="state.beerId"/></div>`;
static components = { Beer };
state = useState({ beerId: 1 });
}
const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } };
const store = new Store({ state });
(<any>env).store = store;
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
app.state.beerId = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>kwak</span></div>");
});
test("connected component is updated when store is changed", async () => {
class App extends Component<any, any> {
static template = xml`
<div>
<span t-foreach="data.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span>
</div>`;
// we have here a new object
data = useStore(state => ({ beers: state.beers, otherKey: 1 }));
}
const actions = {
addBeer({ state }, name) {
state.beers.push({ name });
}
};
const state = { beers: [{ name: "jupiler" }] };
const store = new Store({ state, actions });
(<any>env).store = store;
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
store.dispatch("addBeer", "kwak");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>kwak</span></div>");
});
test("connected component with undefined, null and string props", async () => {
class Beer extends Component<any, any> {
static template = xml`
<div t-name="Beer">
<span>taster:<t t-esc="data.taster"/></span>
<span t-if="data.selected">selected:<t t-esc="data.selected.name"/></span>
<span t-if="data.consumed">consumed:<t t-esc="data.consumed.name"/></span>
</div>`;
data = useStore((state, props) => ({
selected: state.beers[props.id],
consumed: state.beers[state.consumedID] || null,
taster: state.taster
}));
}
class App extends Component<any, any> {
static template = xml`<div><Beer id="state.beerId"/></div>`;
static components = { Beer };
state = useState({ beerId: 0 });
}
const actions = {
consume({ state }, beerId) {
state.consumedID = beerId;
}
};
const state = {
beers: {
1: { name: "jupiler" }
},
consumedID: null,
taster: "aaron"
};
const store = new Store({ state, actions });
(<any>env).store = store;
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>");
app.state.beerId = 1;
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>selected:jupiler</span></div></div>"
);
store.dispatch("consume", 1);
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>selected:jupiler</span><span>consumed:jupiler</span></div></div>"
);
app.state.beerId = 0;
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>consumed:jupiler</span></div></div>"
);
});
test("connected component deeply reactive with undefined, null and string props", async () => {
class Beer extends Component<any, any> {
static template = xml`
<div>
<span>taster:<t t-esc="info.taster"/></span>
<span t-if="info.selected">selected:<t t-esc="info.selected.name"/></span>
<span t-if="info.consumed">consumed:<t t-esc="info.consumed.name"/></span>
</div>`;
info = useStore(function(state, props) {
return {
selected: state.beers[props.id],
consumed: state.beers[state.consumedID] || null,
taster: state.taster
};
});
}
class App extends Component<any, any> {
static template = xml`<div><Beer id="state.beerId"/></div>`;
static components = { Beer };
state = useState({ beerId: 0 });
}
const actions = {
changeTaster({ state }, newTaster) {
state.taster = newTaster;
},
consume({ state }, beerId) {
state.consumedID = beerId;
},
renameBeer({ state }, { beerId, name }) {
state.beers[beerId].name = name;
}
};
const state = {
beers: {
1: { name: "jupiler" }
},
consumedID: null,
taster: "aaron"
};
const store = new Store({ state, actions });
(<any>env).store = store;
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>");
app.state.beerId = 1;
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>selected:jupiler</span></div></div>"
);
store.dispatch("renameBeer", { beerId: 1, name: "kwak" });
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>selected:kwak</span></div></div>"
);
store.dispatch("consume", 1);
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>selected:kwak</span><span>consumed:kwak</span></div></div>"
);
app.state.beerId = 0;
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>consumed:kwak</span></div></div>"
);
store.dispatch("renameBeer", { beerId: 1, name: "jupiler" });
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span><span>consumed:jupiler</span></div></div>"
);
store.dispatch("changeTaster", "matthieu");
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:matthieu</span><span>consumed:jupiler</span></div></div>"
);
});
test("correct update order when parent/children are connected", async () => {
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="state.msg"/></span>`;
state = useStore((state, props) => {
steps.push("child");
return { msg: state.msg[props.key] };
});
}
class Parent extends Component<any, any> {
static template = xml`<div><Child key="state.current"/></div>`;
static components = { Child };
state = useStore(state => {
steps.push("parent");
return {
current: state.current,
isvisible: state.isvisible
};
});
}
const state = { current: "a", msg: { a: "a", b: "b" } };
const actions = {
setCurrent({ state }, c) {
state.current = c;
}
};
const store = new Store({ state, actions });
(<any>env).store = store;
const app = new Parent(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>a</span></div>");
expect(steps).toEqual(["parent", "child"]);
store.dispatch("setCurrent", "b");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>b</span></div>");
expect(steps).toEqual(["parent", "child", "parent", "child"]);
});
test("correct update order when parent/children are connected, part 2", async () => {
const steps: string[] = [];
let def = makeDeferred();
def.resolve();
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="state.msg"/></span>`;
state = useStore((s, props) => {
steps.push("child");
return { msg: s.messages[props.someId] };
});
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<Child t-if="state.flag" someId="state.someId"/>
</div>`;
static components = { Child };
state = useStore(s => {
steps.push("parent");
return { flag: s.flag, someId: s.someId };
});
async render(force) {
await def;
return super.render(force);
}
}
const state = { someId: 1, flag: true, messages: { 1: "abc" } };
const actions = {
setFlagToFalse({ state }) {
state.flag = false;
}
};
const store = new Store({ state, actions });
(<any>env).store = store;
const app = new Parent(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>abc</span></div>");
expect(steps).toEqual(["parent", "child"]);
def = makeDeferred();
store.dispatch("setFlagToFalse");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>abc</span></div>");
expect(steps).toEqual(["parent", "child", "parent"]);
def.resolve();
await nextTick();
expect(steps).toEqual(["parent", "child", "parent"]);
expect(fixture.innerHTML).toBe("<div></div>");
});
test("connected parent/children: no double rendering", async () => {
let steps: string[] = [];
const actions = {
editTodo({ state }) {
state.todos[1].title = "abc";
}
};
const todos = { 1: { id: 1, title: "kikoou" } };
const state = {
todos
};
const store = new Store({
state,
actions
});
class TodoItem extends Component<any, any> {
static template = xml`
<div class="todo">
<t t-esc="state.todo.title"/>
<button class="destroy" t-on-click="editTodo">x</button>
</div>`;
state = useStore((state, props) => {
steps.push("item:usestore");
return {
todo: state.todos[props.id]
};
});
editTodo() {
this.env.store.dispatch("editTodo");
}
__render(f) {
steps.push("item:render");
return super.__render(f);
}
}
class TodoApp extends Component<any, any> {
static template = xml`
<div class="todoapp">
<t t-foreach="Object.values(state.todos)" t-as="todo">
<TodoItem t-key="todo.id" id="todo.id"/>
</t>
</div>`;
static components = { TodoItem };
state = useStore(state => {
steps.push("app:usestore");
return { todos: state.todos };
});
__render(f) {
steps.push("app:render");
return super.__render(f);
}
}
(<any>env).store = store;
const app = new TodoApp(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div class="todoapp"><div class="todo">kikoou<button class="destroy">x</button></div></div>'
);
expect(steps).toEqual(["app:usestore", "app:render", "item:usestore", "item:render"]);
steps = [];
fixture.querySelector("button")!.click();
await nextTick();
expect(steps).toEqual(["app:usestore", "app:render", "item:usestore", "item:render"]);
expect(fixture.innerHTML).toBe(
'<div class="todoapp"><div class="todo">abc<button class="destroy">x</button></div></div>'
);
});
test("connected parent/children: no rendering if child is destroyed", async () => {
let steps: string[] = [];
const actions = {
removeTodo({ state }) {
delete state.todos[1];
}
};
const todos = { 1: { id: 1, title: "kikoou" } };
const state = {
todos
};
const store = new Store({
state,
actions
});
class TodoItem extends Component<any, any> {
static template = xml`
<div class="todo">
<t t-esc="state.todo.title"/>
<button class="destroy" t-on-click="removeTodo">x</button>
</div>`;
state = useStore((state, props) => {
steps.push("item:usestore");
return {
todo: state.todos[props.id]
};
});
removeTodo() {
this.env.store.dispatch("removeTodo");
}
__render(f) {
steps.push("item:render");
return super.__render(f);
}
}
class TodoApp extends Component<any, any> {
static template = xml`
<div class="todoapp">
<t t-foreach="Object.values(state.todos)" t-as="todo">
<TodoItem t-key="todo.id" id="todo.id"/>
</t>
</div>`;
static components = { TodoItem };
state = useStore(state => {
steps.push("app:usestore");
return { todos: state.todos };
});
__render(f) {
steps.push("app:render");
return super.__render(f);
}
}
(<any>env).store = store;
const app = new TodoApp(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div class="todoapp"><div class="todo">kikoou<button class="destroy">x</button></div></div>'
);
expect(steps).toEqual(["app:usestore", "app:render", "item:usestore", "item:render"]);
fixture.querySelector("button")!.click();
await nextTick();
expect(steps).toEqual([
"app:usestore",
"app:render",
"item:usestore",
"item:render",
"app:usestore",
"app:render"
]);
expect(fixture.innerHTML).toBe('<div class="todoapp"></div>');
});
test("connected component willpatch/patch hooks are called on store updates", async () => {
const steps: string[] = [];
class App extends Component<any, any> {
static template = xml`<div><t t-esc="store.msg"/></div>`;
store = useStore(s => ({ msg: s.msg }));
willPatch() {
steps.push("willpatch");
}
patched() {
steps.push("patched");
}
}
const state = { msg: "a" };
const actions = {
setMsg({ state }, c) {
state.msg = c;
}
};
const store = new Store({ state, actions });
(<any>env).store = store;
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>a</div>");
store.dispatch("setMsg", "b");
await nextTick();
expect(fixture.innerHTML).toBe("<div>b</div>");
expect(steps).toEqual(["willpatch", "patched"]);
});
test("connected child components stop listening to store when destroyed", async () => {
let steps: any = [];
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="store.val"/></div>`;
store = useStore(s => s);
}
class Parent extends Component<any, any> {
static template = xml`<div><Child t-if="state.child" /></div>`;
static components = { Child };
state = useState({ child: true });
}
class TestStore extends Store {
on(eventType, owner, callback) {
steps.push(`on:${eventType}`);
super.on(eventType, owner, callback);
}
off(eventType, owner) {
steps.push(`off:${eventType}`);
super.off(eventType, owner);
}
}
const store = new TestStore({ state: { val: 1 } });
(<any>env).store = store;
const parent = new Parent(env);
await parent.mount(fixture);
expect(steps).toEqual(["on:update"]);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
parent.state.child = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["on:update", "off:update"]);
});
test("dispatch an action", async () => {
class App extends Component<any, any> {
static template = xml`<div><t t-esc="store.counter"/></div>`;
store = useStore(state => state);
dispatch = useDispatch();
}
const state = {
counter: 0
};
const actions = {
inc({ state }) {
return ++state.counter;
}
};
const store = new Store({ state, actions });
(<any>env).store = store;
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>0</div>");
const res = app.dispatch("inc");
expect(res).toBe(1);
await nextTick();
expect(fixture.innerHTML).toBe("<div>1</div>");
});
});
describe("various scenarios", () => {
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
test("scenarios with async store updates and some components events", async () => {
const actions = {
async deleteAttachment({ state }) {
await Promise.resolve();
delete state.attachments[100];
state.messages[10].attachmentIds = [];
}
};
const state = {
attachments: {
100: {
id: 100,
name: "text.txt"
}
},
messages: {
10: {
attachmentIds: [100],
id: 10
}
}
};
const store = new Store({ actions, state });
class Attachment extends Component<any, any> {
static template = xml`
<div>
<span>Attachment <t t-esc="props.id"/></span>
<span>Name: <t t-esc="attachment.name"/></span>
</div>`;
attachment = useStore((state, props) => ({ name: state.attachments[props.id].name }));
}
class Message extends Component<any, any> {
static template = xml`
<div>
<button t-on-click="doStuff">Do stuff</button>
<Attachment t-foreach="store.attachmentIds" t-key="attachmentId" t-as="attachmentId" id="attachmentId"/>
</div>`;
store = useStore(state => ({ attachmentIds: state.messages[10].attachmentIds }));
static components = { Attachment };
state = { isAttachmentDeleted: false };
dispatch = useDispatch();
doStuff() {
this.dispatch("deleteAttachment", 100);
this.state.isAttachmentDeleted = true;
}
}
(<any>env).store = store;
const message = new Message(env);
await message.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.innerHTML).toMatchSnapshot();
});
});
+50
View File
@@ -1,4 +1,5 @@
import { h, patch } from "../src/vdom";
import { htmlToVDOM } from "../src/vdom/html_to_vdom";
import { init, addNS } from "../src/vdom/vdom";
function map(list, fn) {
@@ -1104,3 +1105,52 @@ describe("snabbdom", function() {
});
});
});
//------------------------------------------------------------------------------
// Html to vdom
//------------------------------------------------------------------------------
describe("html to vdom", function() {
let elm, vnode0;
beforeEach(function() {
elm = document.createElement("div");
vnode0 = elm;
});
test("empty strings return empty list", function() {
expect(htmlToVDOM("")).toEqual([]);
});
test("just text", function() {
const nodeList = htmlToVDOM("simple text");
expect(nodeList).toHaveLength(1);
expect(nodeList[0]).toEqual({ text: "simple text" });
});
test("empty tag", function() {
const nodeList = htmlToVDOM("<span></span>");
expect(nodeList).toHaveLength(1);
elm = patch(vnode0, nodeList[0]).elm;
expect(elm.outerHTML).toEqual("<span></span>");
});
test("tag with text", function() {
const nodeList = htmlToVDOM("<span>abc</span>");
expect(nodeList).toHaveLength(1);
elm = patch(vnode0, nodeList[0]).elm;
expect(elm.outerHTML).toEqual("<span>abc</span>");
});
test("tag with attribute", function() {
const nodeList = htmlToVDOM(`<span a="1" b="2">abc</span>`);
expect(nodeList).toHaveLength(1);
elm = patch(vnode0, nodeList[0]).elm;
expect(elm.outerHTML).toEqual(`<span a="1" b="2">abc</span>`);
});
test("misc", function() {
const nodeList = htmlToVDOM(`<span a="1" b="2">abc<div>1</div></span>`);
expect(nodeList).toHaveLength(1);
elm = patch(vnode0, nodeList[0]).elm;
expect(elm.outerHTML).toEqual(`<span a="1" b="2">abc<div>1</div></span>`);
});
});
+84 -24
View File
@@ -1,5 +1,5 @@
import { SAMPLES } from "./samples.js";
const {useState, useRef} = owl.hooks;
const { useState, useRef, onMounted, onWillUnmount } = owl.hooks;
//------------------------------------------------------------------------------
// Constants, helpers, utils
//------------------------------------------------------------------------------
@@ -150,6 +150,51 @@ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start);
return zip.generateAsync({ type: "blob" });
}
//------------------------------------------------------------------------------
// SAMPLES
//------------------------------------------------------------------------------
function loadSamples() {
let result = SAMPLES.slice();
const localSample = localStorage.getItem("owl-playground-local-sample");
if (localSample) {
const { js, css, xml } = JSON.parse(localSample);
result.unshift({
description: "Local Storage Code",
code: js,
xml,
css
});
}
return result;
}
function saveLocalSample(js, css, xml) {
const str = JSON.stringify({ js, css, xml });
localStorage.setItem("owl-playground-local-sample", str);
}
function deleteLocalSample() {
localStorage.removeItem("owl-playground-local-sample");
}
function useSamples() {
const samples = loadSamples();
const component = owl.Component.current;
let interval;
onMounted(() => {
const state = component.state;
interval = setInterval(() => {
if (component.isDirty) {
saveLocalSample(state.js, state.css, state.xml);
}
}, 1000);
});
onWillUnmount(() => {
clearInterval(interval);
});
return samples;
}
//------------------------------------------------------------------------------
// Tabbed editor
//------------------------------------------------------------------------------
@@ -157,13 +202,14 @@ class TabbedEditor extends owl.Component {
constructor(parent, props) {
super(parent, props);
this.state = useState({
currentTab: props.js ? "js" : props.xml ? "xml" : "css"
currentTab: props.js !== false ? "js" : props.xml ? "xml" : "css"
});
this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.sessions = {};
this._setupSessions(props);
this.editorNode = useRef("editor");
this._updateCode = this._updateCode.bind(this);
}
mounted() {
@@ -175,18 +221,14 @@ class TabbedEditor extends owl.Component {
this.editor.setSession(this.sessions[this.state.currentTab]);
const tabSize = this.state.currentTab === "xml" ? 2 : 4;
this.editor.session.setOption("tabSize", tabSize);
this.editor.on("blur", () => {
const editorValue = this.editor.getValue();
const propsValue = this.props[this.state.currentTab];
if (editorValue !== propsValue) {
this.trigger("updateCode", {
type: this.state.currentTab,
value: editorValue
});
}
});
this.editor.on("blur", this._updateCode);
this.interval = setInterval(this._updateCode, 3000);
}
willUnmount() {
clearInterval(this.interval);
this.editor.off("blur", this._updateCode);
}
willUpdateProps(nextProps) {
this._setupSessions(nextProps);
}
@@ -195,13 +237,15 @@ class TabbedEditor extends owl.Component {
const session = this.sessions[this.state.currentTab];
let content = this.props[this.state.currentTab];
if (content === false) {
const tab = this.props.js ? "js" : this.props.xml ? "xml" : "css";
const tab = this.props.js !== false ? "js" : this.props.xml ? "xml" : "css";
content = this.props[tab];
this.state.currentTab = tab;
}
session.setValue(content, -1);
this.editor.setSession(session);
this.editor.resize();
if (this.editor.getValue() !== content) {
session.setValue(content, -1);
this.editor.setSession(session);
this.editor.resize();
}
}
setTab(tab) {
@@ -230,7 +274,7 @@ class TabbedEditor extends owl.Component {
_setupSessions(props) {
for (let tab of ["js", "xml", "css"]) {
if (props[tab] && !this.sessions[tab]) {
if (props[tab] !== false && !this.sessions[tab]) {
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
this.sessions[tab].setOption("useWorker", false);
const tabSize = tab === "xml" ? 2 : 4;
@@ -239,6 +283,17 @@ class TabbedEditor extends owl.Component {
}
}
}
_updateCode() {
const editorValue = this.editor.getValue();
const propsValue = this.props[this.state.currentTab];
if (editorValue !== propsValue) {
this.trigger("updateCode", {
type: this.state.currentTab,
value: editorValue
});
}
}
}
//------------------------------------------------------------------------------
@@ -248,12 +303,13 @@ class App extends owl.Component {
constructor(...args) {
super(...args);
this.version = owl.__info__.version;
this.SAMPLES = SAMPLES;
this.SAMPLES = useSamples();
this.isDirty = false;
this.state = useState({
js: SAMPLES[0].code,
css: SAMPLES[0].css || "",
xml: SAMPLES[0].xml || DEFAULT_XML,
js: this.SAMPLES[0].code,
css: this.SAMPLES[0].css || "",
xml: this.SAMPLES[0].xml || DEFAULT_XML,
error: false,
displayWelcome: true,
splitLayout: true,
@@ -302,10 +358,12 @@ class App extends owl.Component {
}
setSample(ev) {
const sample = SAMPLES.find(s => s.description === ev.target.value);
const sample = this.SAMPLES.find(s => s.description === ev.target.value);
this.state.js = sample.code;
this.state.css = sample.css || "";
this.state.xml = sample.xml || DEFAULT_XML;
deleteLocalSample();
this.isDirty = false;
}
get leftPaneStyle() {
@@ -338,7 +396,10 @@ class App extends owl.Component {
});
}
updateCode(ev) {
this.state[ev.detail.type] = ev.detail.value;
if (this.state[ev.detail.type] !== ev.detail.value) {
this.state[ev.detail.type] = ev.detail.value;
this.isDirty = true;
}
}
toggleLayout() {
this.state.splitLayout = !this.state.splitLayout;
@@ -363,7 +424,6 @@ class App extends owl.Component {
}
App.components = { TabbedEditor };
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
+3 -1
View File
@@ -173,8 +173,10 @@ body {
}
.right-pane .note {
font-size: 20px;
font-size: 18px;
text-align: left;
margin-top: 20px;
padding: 5%;
}
.right-pane .error {
+152 -93
View File
@@ -5,7 +5,7 @@ class Greeter extends Component {
state = useState({ word: 'Hello' });
toggle() {
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
}
}
@@ -318,6 +318,66 @@ const HOOKS_CSS = `button {
font-size: 16px;
}`;
const CONTEXT_JS = `// In this example, we show how components can use the Context and 'useContext'
// hook to share information between them.
const { Component, Context } = owl;
const { useContext } = owl.hooks;
class ToolbarButton extends Component {
theme = useContext(this.env.themeContext);
get style () {
const theme = this.theme;
return \`background-color: \${theme.background}; color: \${theme.foreground}\`;
}
}
class Toolbar extends Component {
static components = { ToolbarButton };
}
// Main root component
class App extends Component {
static components = { Toolbar };
toggleTheme() {
const { background, foreground } = this.env.themeContext.state;
this.env.themeContext.state.background = foreground;
this.env.themeContext.state.foreground = background;
}
}
// Application setup
const themeContext = new Context({
background: '#000',
foreground: '#fff',
});
const env = {
qweb: new owl.QWeb(TEMPLATES),
themeContext: themeContext,
};
const app = new App(env);
app.mount(document.body);
`;
const CONTEXT_XML = `<templates>
<button t-name="ToolbarButton" t-att-style="style">
<t t-esc="props.name"/>
</button>
<div t-name="Toolbar">
<ToolbarButton name="'A'"/>
<ToolbarButton name="'B'"/>
<ToolbarButton name="'C'"/>
</div>
<div t-name="App">
<button t-on-click="toggleTheme">Toggle Mode</button>
<Toolbar/>
</div>
</templates>
`;
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.
@@ -325,99 +385,94 @@ const TODO_APP_STORE = `// This example is an implementation of the TodoList app
// In this implementation, we use the owl Store class to manage the state. It
// is very similar to the VueX store.
const { Component, useState } = owl;
const { useRef } = owl.hooks;
const { useRef, useStore, useDispatch, onPatched, onMounted } = owl.hooks;
//------------------------------------------------------------------------------
// Constants, helpers
//------------------------------------------------------------------------------
const ENTER_KEY = 13;
const ESC_KEY = 27;
const LOCALSTORAGE_KEY = "todomvc";
function useAutofocus(name) {
let ref = useRef(name);
let isInDom = false;
function updateFocus() {
if (!isInDom && ref.el) {
isInDom = true;
const current = ref.el.value;
ref.el.value = "";
ref.el.focus();
ref.el.value = current;
} else if (isInDom && !ref.el) {
isInDom = false;
}
}
onPatched(updateFocus);
onMounted(updateFocus);
}
//------------------------------------------------------------------------------
// Store Definition
// Store
//------------------------------------------------------------------------------
const initialState = { todos: [], nextId: 1};
const actions = {
addTodo({ state }, title) {
state.todos.push({
const todo = {
id: state.nextId++,
title,
completed: false
});
}
state.todos.push(todo);
},
removeTodo({ state }, id) {
const index = state.todos.findIndex(t => t.id === id);
state.todos.splice(index, 1);
},
toggleTodo({ state, dispatch }, id) {
updateTodo({state, dispatch}, {id, title}) {
const value = title.trim();
if (!value) {
dispatch('removeTodo', id);
} else {
const todo = state.todos.find(t => t.id === id);
todo.title = value;
}
},
toggleTodo({ state }, id) {
const todo = state.todos.find(t => t.id === id);
dispatch("editTodo", { id, completed: !todo.completed });
todo.completed = !todo.completed;
},
clearCompleted({ state, dispatch }) {
state.todos
.filter(todo => todo.completed)
.forEach(todo => {
for (let todo of state.todos) {
if (todo.completed) {
dispatch("removeTodo", todo.id);
});
}
}
},
toggleAll({ state, dispatch }, completed) {
state.todos.forEach(todo => {
dispatch("editTodo", {
id: todo.id,
completed
});
});
},
editTodo({ state }, { id, title, completed }) {
const todo = state.todos.find(t => t.id === id);
if (title !== undefined) {
todo.title = title;
}
if (completed !== undefined) {
for (let todo of state.todos) {
todo.completed = completed;
}
}
},
};
function saveState(state) {
const str = JSON.stringify(state);
window.localStorage.setItem(LOCALSTORAGE_KEY, str);
}
function loadState() {
const localState = window.localStorage.getItem(LOCALSTORAGE_KEY);
return localState ? JSON.parse(localState) : { todos: [], nextId: 1};
}
function makeStore() {
const state = loadState();
const store = new owl.store.Store({ state, actions });
store.on("update", null, () => saveState(store.state));
return store;
}
//------------------------------------------------------------------------------
// TodoItem
//------------------------------------------------------------------------------
class TodoItem extends Component {
state = useState({ isEditing: false });
inputRef = useRef("input");
dispatch = useDispatch();
removeTodo() {
this.env.store.dispatch("removeTodo", this.props.id);
constructor(...args) {
super(...args);
useAutofocus("input");
}
toggleTodo() {
this.env.store.dispatch("toggleTodo", this.props.id);
}
async editTodo() {
editTodo() {
this.state.isEditing = true;
}
focusInput() {
this.inputRef.el.value = "";
this.inputRef.el.focus();
this.inputRef.el.value = this.props.title;
}
handleKeyup(ev) {
if (ev.keyCode === ENTER_KEY) {
this.updateTitle(ev.target.value);
@@ -433,48 +488,34 @@ class TodoItem extends Component {
}
updateTitle(title) {
const value = title.trim();
if (!value) {
this.removeTodo(this.props.id);
} else {
this.env.store.dispatch("editTodo", {
id: this.props.id,
title: value
});
this.state.isEditing = false;
}
this.dispatch("updateTodo", {title, id: this.props.id});
this.state.isEditing = false;
}
}
//------------------------------------------------------------------------------
// TodoApp
//------------------------------------------------------------------------------
class TodoApp extends owl.store.ConnectedComponent {
class TodoApp extends Component {
static components = { TodoItem };
state = useState({ filter: "all" });
todos = useStore(state => state.todos);
dispatch = useDispatch();
static mapStoreToProps(state) {
return {
todos: state.todos
};
}
get visibleTodos() {
let todos = this.storeProps.todos;
if (this.state.filter === "active") {
todos = todos.filter(t => !t.completed);
switch (this.state.filter) {
case "active": return this.todos.filter(t => !t.completed);
case "completed": return this.todos.filter(t => t.completed);
case "all": return this.todos;
}
if (this.state.filter === "completed") {
todos = todos.filter(t => t.completed);
}
return todos;
}
get allChecked() {
return this.storeProps.todos.every(todo => todo.completed);
return this.todos.every(todo => todo.completed);
}
get remaining() {
return this.storeProps.todos.filter(todo => !todo.completed).length;
return this.todos.filter(todo => !todo.completed).length;
}
get remainingText() {
@@ -500,12 +541,25 @@ class TodoApp extends owl.store.ConnectedComponent {
//------------------------------------------------------------------------------
// App Initialization
//------------------------------------------------------------------------------
const store = makeStore();
const qweb = new owl.QWeb(TEMPLATES);
const env = {
qweb,
store,
};
function saveState(state) {
const str = JSON.stringify(state);
window.localStorage.setItem(LOCALSTORAGE_KEY, str);
}
function loadState() {
const localState = window.localStorage.getItem(LOCALSTORAGE_KEY);
return localState ? JSON.parse(localState) : initialState;
}
function makeEnv() {
const state = loadState();
const store = new owl.Store({ state, actions });
store.on("update", null, () => saveState(store.state));
const qweb = new owl.QWeb(TEMPLATES);
return { qweb, store };
}
const env = makeEnv();
const app = new TodoApp(env);
app.mount(document.body);
`;
@@ -516,7 +570,7 @@ const TODO_APP_STORE_XML = `<templates>
<h1>todos</h1>
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
</header>
<section class="main" t-if="storeProps.todos.length">
<section class="main" t-if="todos.length">
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="dispatch('toggleAll', !allChecked)"/>
<label for="toggle-all"></label>
<ul class="todo-list">
@@ -525,7 +579,7 @@ const TODO_APP_STORE_XML = `<templates>
</t>
</ul>
</section>
<footer class="footer" t-if="storeProps.todos.length">
<footer class="footer" t-if="todos.length">
<span class="todo-count">
<strong>
<t t-esc="remaining"/>
@@ -543,7 +597,7 @@ const TODO_APP_STORE_XML = `<templates>
<a t-on-click="setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a>
</li>
</ul>
<button class="clear-completed" t-if="storeProps.todos.length gt remaining" t-on-click="dispatch('clearCompleted')">
<button class="clear-completed" t-if="todos.length gt remaining" t-on-click="dispatch('clearCompleted')">
Clear completed
</button>
</footer>
@@ -551,13 +605,13 @@ const TODO_APP_STORE_XML = `<templates>
<li t-name="TodoItem" class="todo" t-att-class="{completed: props.completed, editing: state.isEditing}">
<div class="view">
<input class="toggle" type="checkbox" t-on-change="toggleTodo" t-att-checked="props.completed"/>
<input class="toggle" type="checkbox" t-on-change="dispatch('toggleTodo', props.id)" t-att-checked="props.completed"/>
<label t-on-dblclick="editTodo">
<t t-esc="props.title"/>
</label>
<button class="destroy" t-on-click="removeTodo"></button>
<button class="destroy" t-on-click="dispatch('removeTodo', props.id)"></button>
</div>
<input class="edit" t-ref="input" t-if="state.isEditing" t-att-value="props.title" t-on-keyup="handleKeyup" t-mounted="focusInput" t-on-blur="handleBlur"/>
<input class="edit" t-ref="input" t-if="state.isEditing" t-att-value="props.title" t-on-keyup="handleKeyup" t-on-blur="handleBlur"/>
</li>
</templates>`;
@@ -1655,6 +1709,11 @@ export const SAMPLES = [
xml: HOOKS_DEMO_XML,
css: HOOKS_CSS
},
{
description: "Context",
code: CONTEXT_JS,
xml: CONTEXT_XML,
},
{
description: "Todo List App (with store)",
code: TODO_APP_STORE,
+5 -1
View File
@@ -45,7 +45,11 @@
<div>🦉 Odoo Web Library 🦉</div>
<div>v<t t-esc="version"/></div>
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
<div class="note">Note: these examples require a recent browser to work without a transpilation step. </div>
<div class="note">
<p>Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step!
For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version.
</p>
</div>
</div>
<div t-if="state.error" class="error">
<h3>Error</h3>