mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e65b0686f | |||
| 1d72164015 | |||
| 2825a5a55d | |||
| d40ff53f5a | |||
| 23246c42aa | |||
| 895fe7c60f | |||
| 19b10f7c1d | |||
| 0788d9fe75 | |||
| 4a346d0615 | |||
| 1337aa0107 | |||
| 3e8f60cefa | |||
| 41b0618c93 | |||
| 0928c189f4 | |||
| 2f27768be2 | |||
| aa1b0f502d | |||
| da1824f4dd | |||
| 07dac970f7 | |||
| 5f6f081ae2 | |||
| 1c3c2e5c51 | |||
| 17ce73e6a9 | |||
| cedb1f411f | |||
| e7516be95c | |||
| 683993dbc4 | |||
| 50d569411c |
@@ -87,7 +87,11 @@ find some more information [here](doc/comparison.md).
|
|||||||
|
|
||||||
## Documentation
|
## 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)
|
- [Quick Start](doc/quick_start.md)
|
||||||
- [Component](doc/component.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:
|
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.23.0.js](https://github.com/odoo/owl/releases/download/v0.23.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.min.js](https://github.com/odoo/owl/releases/download/v0.23.0/owl.min.js)
|
||||||
|
|
||||||
Some npm scripts are available:
|
Some npm scripts are available:
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -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
|
### Methods
|
||||||
|
|
||||||
We explain here all the public methods of the `Component` class.
|
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
|
always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
|
||||||
can be useful if we are handling large number of components.
|
can be useful if we are handling large number of components.
|
||||||
|
|
||||||
- **`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,
|
- **`destroy()`**. As its name suggests, this method will remove the component,
|
||||||
and perform all necessary cleanup, such as unmounting the component, its children,
|
and perform all necessary cleanup, such as unmounting the component, its children,
|
||||||
removing the parent/children relationship. This method should almost never be
|
removing the parent/children relationship. This method should almost never be
|
||||||
|
|||||||
+108
@@ -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
@@ -5,7 +5,7 @@ parts of the code. Owl has a very simple bus class, which manages subscriptions,
|
|||||||
triggering events, and callbacks.
|
triggering events, and callbacks.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const bus = new owl.EventBus();
|
const bus = new owl.core.EventBus();
|
||||||
|
|
||||||
bus.on("some-event", null, function(...args) {
|
bus.on("some-event", null, function(...args) {
|
||||||
console.log(...args);
|
console.log(...args);
|
||||||
|
|||||||
+152
-5
@@ -12,8 +12,15 @@
|
|||||||
- [`onWillUnmount`](#onwillunmount)
|
- [`onWillUnmount`](#onwillunmount)
|
||||||
- [`onWillPatch`](#onwillpatch)
|
- [`onWillPatch`](#onwillpatch)
|
||||||
- [`onPatched`](#onpatched)
|
- [`onPatched`](#onpatched)
|
||||||
|
- [`onWillStart`](#onwillstart)
|
||||||
|
- [`onWillUpdateProps`](#onwillupdateprops)
|
||||||
|
- [`useContext`](#usecontext)
|
||||||
- [`useRef`](#useref)
|
- [`useRef`](#useref)
|
||||||
- [`useSubEnv`](#useSubEnv)
|
- [`useSubEnv`](#usesubenv)
|
||||||
|
- [`useStore`](#usestore)
|
||||||
|
- [`useDispatch`](#usedispatch)
|
||||||
|
- [`useGetters`](#usegetters)
|
||||||
|
- [Making customized hooks](#making-customized-hooks)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -145,6 +152,10 @@ class SomeComponent extends Component {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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`
|
### `useState`
|
||||||
|
|
||||||
The `useState` hook is certainly the most important hooks for Owl components:
|
The `useState` hook is certainly the most important hooks for Owl components:
|
||||||
@@ -182,7 +193,6 @@ is mounted (see example on top of this page).
|
|||||||
abstractions. `onWillUnmount` registers a callback, which will be called when the component
|
abstractions. `onWillUnmount` registers a callback, which will be called when the component
|
||||||
is unmounted (see example on top of this page).
|
is unmounted (see example on top of this page).
|
||||||
|
|
||||||
|
|
||||||
### `onWillPatch`
|
### `onWillPatch`
|
||||||
|
|
||||||
`onWillPatch` is not an user hook, but is a building block designed to help make useful
|
`onWillPatch` is not an user hook, but is a building block designed to help make useful
|
||||||
@@ -195,6 +205,63 @@ before the component patched.
|
|||||||
abstractions. `onPatched` registers a callback, which will be called just
|
abstractions. `onPatched` registers a callback, which will be called just
|
||||||
after the component patched.
|
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`
|
### `useRef`
|
||||||
|
|
||||||
The `useRef` hook is useful when we need a way to interact with some inside part
|
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||||
@@ -251,7 +318,7 @@ If this is not the case, accessing `el` or `comp` on it will return `null`.
|
|||||||
### `useSubEnv`
|
### `useSubEnv`
|
||||||
|
|
||||||
The environment is sometimes useful to share some common information between
|
The environment is sometimes useful to share some common information between
|
||||||
all components. But sometimes, we want to *scope* that knowledge to a subtree.
|
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
|
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.
|
`model` object available to all sub component, but not to the whole application.
|
||||||
@@ -271,5 +338,85 @@ class FormComponent extends Component {
|
|||||||
|
|
||||||
The `useSubEnv` takes one argument: an object which contains some key/value that
|
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
|
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
|
the parent environment. And of course, the parent environment will not be
|
||||||
affected.
|
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.
|
||||||
|
|||||||
@@ -285,6 +285,11 @@ rendered with the value `value` set to `<span>foo</span>` in the rendering conte
|
|||||||
<p><span>foo</span></p>
|
<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
|
### 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, ...
|
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, ...
|
||||||
|
|||||||
+11
-4
@@ -8,26 +8,31 @@ build applications. Here is a complete representation of its content:
|
|||||||
```
|
```
|
||||||
owl
|
owl
|
||||||
Component
|
Component
|
||||||
|
Context
|
||||||
QWeb
|
QWeb
|
||||||
|
Store
|
||||||
useState
|
useState
|
||||||
core
|
core
|
||||||
EventBus
|
EventBus
|
||||||
Observer
|
Observer
|
||||||
hooks
|
hooks
|
||||||
|
onWillStart
|
||||||
onMounted
|
onMounted
|
||||||
onWillUnmount
|
onWillUpdateProps
|
||||||
onWillPatch
|
onWillPatch
|
||||||
onPatched
|
onPatched
|
||||||
|
onWillUnmount
|
||||||
|
useContext
|
||||||
useState
|
useState
|
||||||
useRef
|
useRef
|
||||||
useSubEnv
|
useSubEnv
|
||||||
|
useStore
|
||||||
|
useDispatch
|
||||||
|
useGetters
|
||||||
router
|
router
|
||||||
Link
|
Link
|
||||||
RouteComponent
|
RouteComponent
|
||||||
Router
|
Router
|
||||||
store
|
|
||||||
Store
|
|
||||||
ConnectedComponent
|
|
||||||
tags
|
tags
|
||||||
xml
|
xml
|
||||||
utils
|
utils
|
||||||
@@ -38,12 +43,14 @@ owl
|
|||||||
whenReady
|
whenReady
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
|
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
- [Animations](animations.md)
|
- [Animations](animations.md)
|
||||||
- [Component](component.md)
|
- [Component](component.md)
|
||||||
|
- [Context](context.md)
|
||||||
- [Event Bus](event_bus.md)
|
- [Event Bus](event_bus.md)
|
||||||
- [Hooks](hooks.md)
|
- [Hooks](hooks.md)
|
||||||
- [Observer](observer.md)
|
- [Observer](observer.md)
|
||||||
|
|||||||
+53
-52
@@ -9,6 +9,7 @@
|
|||||||
- [Actions](#actions)
|
- [Actions](#actions)
|
||||||
- [Getters](#getters)
|
- [Getters](#getters)
|
||||||
- [Connecting a Component](#connecting-a-component)
|
- [Connecting a Component](#connecting-a-component)
|
||||||
|
- [`useStore`](#usestore)
|
||||||
- [Semantics](#semantics)
|
- [Semantics](#semantics)
|
||||||
- [Good Practices](#good-practices)
|
- [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.
|
component should own which part of the state.
|
||||||
|
|
||||||
Owl's solution to this issue is a centralized store. It is a class that owns
|
Owl's solution to this issue is a centralized store. It is a class that owns
|
||||||
some state, and let the developer update it in a structured way, with `actions`.
|
some (or all) state, and let the developer update it in a structured way, with
|
||||||
Owl components can then connect to the store, and will be updated if necessary.
|
`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.
|
Note: Owl store is inspired by React Redux and VueX.
|
||||||
|
|
||||||
@@ -47,7 +49,7 @@ const state = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const store = new owl.Store({ state, actions });
|
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
|
// updating the state
|
||||||
store.dispatch("addTodo", "fix all bugs");
|
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
|
Actions are called with the `dispatch` method on the store, and can receive an
|
||||||
arbitrary number of arguments.
|
arbitrary number of arguments.
|
||||||
|
|
||||||
@@ -195,76 +205,66 @@ is a string or a number.
|
|||||||
|
|
||||||
### Connecting a Component
|
### Connecting a Component
|
||||||
|
|
||||||
At some point, we need a way to access the state in the store from a component.
|
At some point, we need a way to interact with the store from a component. This
|
||||||
By default, an Owl `Component` is not connected to any store. To do that, we
|
can be done with the help of the three store hooks:
|
||||||
need to create a component inheriting from `OwlComponent`:
|
|
||||||
|
- `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
|
```javascript
|
||||||
const actions = {
|
const actions = {
|
||||||
increment({ state }, val) {
|
increment({ state }, val) {
|
||||||
state.counter += val;
|
state.counter.value += val;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
counter: 0
|
counter: {value: 0}
|
||||||
};
|
};
|
||||||
const store = new owl.Store({ state, actions });
|
const store = new owl.Store({ state, actions });
|
||||||
|
```
|
||||||
|
|
||||||
class Counter extends owl.ConnectedComponent {
|
A counter component can then select this value and dispatch an action like this:
|
||||||
static mapStoreToProps(state) {
|
|
||||||
return {
|
```js
|
||||||
value: state.counter
|
class Counter extends Component {
|
||||||
};
|
counter = useStore(state => state.counter);
|
||||||
}
|
dispatch = useDispatch();
|
||||||
increment() {
|
|
||||||
this.env.store.dispatch("increment");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const counter = new Counter({ store, qweb });
|
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
|
```xml
|
||||||
<button t-name="Counter" t-on-click="dispatch('increment')">
|
<button t-name="Counter" t-on-click="dispatch('increment')">
|
||||||
Click Me! [<t t-esc="props.value"/>]
|
Click Me! [<t t-esc="counter.value"/>]
|
||||||
</button>
|
</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
|
### 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:
|
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
|
- 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
|
- avoid asynchronous components as much as possible. Asynchronous components
|
||||||
lead to situations where parts of the UI is not updated immediately.
|
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
|
- 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
|
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
|
for each state update, it is important to make sure that these functions are
|
||||||
as fast as possible.
|
as fast as possible.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "owl-framework",
|
"name": "owl-framework",
|
||||||
"version": "0.22.0",
|
"version": "0.23.0",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+32
-20
@@ -81,11 +81,13 @@ interface Internal<T extends Env, Props> {
|
|||||||
|
|
||||||
boundHandlers: { [key: number]: any };
|
boundHandlers: { [key: number]: any };
|
||||||
observer: Observer | null;
|
observer: Observer | null;
|
||||||
render: CompiledTemplate | null;
|
render: CompiledTemplate;
|
||||||
mountedCB: Function | null;
|
mountedCB: Function | null;
|
||||||
willUnmountCB: Function | null;
|
willUnmountCB: Function | null;
|
||||||
willPatchCB: Function | null;
|
willPatchCB: Function | null;
|
||||||
patchedCB: Function | null;
|
patchedCB: Function | null;
|
||||||
|
willStartCB: Function | null;
|
||||||
|
willUpdatePropsCB: Function | null;
|
||||||
classObj: { [key: string]: boolean } | null;
|
classObj: { [key: string]: boolean } | null;
|
||||||
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
|
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
|
||||||
}
|
}
|
||||||
@@ -99,7 +101,7 @@ export class Component<T extends Env, Props extends {}> {
|
|||||||
readonly __owl__: Internal<Env, Props>;
|
readonly __owl__: Internal<Env, Props>;
|
||||||
static template?: string | null = null;
|
static template?: string | null = null;
|
||||||
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 components = {};
|
||||||
static props?: any;
|
static props?: any;
|
||||||
static defaultProps?: any;
|
static defaultProps?: any;
|
||||||
@@ -140,7 +142,7 @@ export class Component<T extends Env, Props extends {}> {
|
|||||||
*/
|
*/
|
||||||
constructor(parent: Component<T, any> | T, props?: Props) {
|
constructor(parent: Component<T, any> | T, props?: Props) {
|
||||||
const defaultProps = (<any>this.constructor).defaultProps;
|
const defaultProps = (<any>this.constructor).defaultProps;
|
||||||
Component._current = this;
|
Component.current = this;
|
||||||
if (defaultProps) {
|
if (defaultProps) {
|
||||||
props = this.__applyDefaultProps(props, defaultProps);
|
props = this.__applyDefaultProps(props, defaultProps);
|
||||||
}
|
}
|
||||||
@@ -176,6 +178,8 @@ export class Component<T extends Env, Props extends {}> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const qweb = this.env.qweb;
|
||||||
|
|
||||||
this.__owl__ = {
|
this.__owl__ = {
|
||||||
id: id,
|
id: id,
|
||||||
vnode: null,
|
vnode: null,
|
||||||
@@ -190,8 +194,10 @@ export class Component<T extends Env, Props extends {}> {
|
|||||||
willUnmountCB: null,
|
willUnmountCB: null,
|
||||||
willPatchCB: null,
|
willPatchCB: null,
|
||||||
patchedCB: null,
|
patchedCB: null,
|
||||||
|
willStartCB: null,
|
||||||
|
willUpdatePropsCB: null,
|
||||||
observer: null,
|
observer: null,
|
||||||
render: null,
|
render: qweb.render.bind(qweb, this.__getTemplate(qweb)),
|
||||||
classObj: null,
|
classObj: null,
|
||||||
refs: null
|
refs: null
|
||||||
};
|
};
|
||||||
@@ -437,6 +443,9 @@ export class Component<T extends Env, Props extends {}> {
|
|||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
const isMounted = __owl__.isMounted;
|
const isMounted = __owl__.isMounted;
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
|
if (__owl__.willUnmountCB) {
|
||||||
|
__owl__.willUnmountCB();
|
||||||
|
}
|
||||||
this.willUnmount();
|
this.willUnmount();
|
||||||
__owl__.isMounted = false;
|
__owl__.isMounted = false;
|
||||||
}
|
}
|
||||||
@@ -466,7 +475,7 @@ export class Component<T extends Env, Props extends {}> {
|
|||||||
try {
|
try {
|
||||||
this.mounted();
|
this.mounted();
|
||||||
if (__owl__.mountedCB) {
|
if (__owl__.mountedCB) {
|
||||||
__owl__.mountedCB()
|
__owl__.mountedCB();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorHandler(e, this);
|
errorHandler(e, this);
|
||||||
@@ -505,7 +514,10 @@ export class Component<T extends Env, Props extends {}> {
|
|||||||
if (defaultProps) {
|
if (defaultProps) {
|
||||||
nextProps = this.__applyDefaultProps(nextProps, 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;
|
this.props = nextProps;
|
||||||
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
|
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
|
||||||
fiber.patchQueue.push(fiber);
|
fiber.patchQueue.push(fiber);
|
||||||
@@ -535,20 +547,8 @@ export class Component<T extends Env, Props extends {}> {
|
|||||||
return fiber.promise;
|
return fiber.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async __prepareAndRender(fiber: Fiber<Props>): Promise<VNode> {
|
__getTemplate(qweb: QWeb): string {
|
||||||
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;
|
|
||||||
let p = (<any>this).constructor;
|
let p = (<any>this).constructor;
|
||||||
// console.warn(p, p.template, p._template, 'template' in p, p.hasOwnProperty('template'))
|
|
||||||
if (!p.hasOwnProperty("_template")) {
|
if (!p.hasOwnProperty("_template")) {
|
||||||
if (p.template) {
|
if (p.template) {
|
||||||
p._template = p.template;
|
p._template = p.template;
|
||||||
@@ -568,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);
|
return this.__render(fiber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { QWeb } from "../qweb/index";
|
import { QWeb } from "../qweb/index";
|
||||||
import { INTERP_REGEXP } from "../qweb/context";
|
import { INTERP_REGEXP } from "../qweb/compilation_context";
|
||||||
import { MODS_CODE } from "../qweb/extensions";
|
import { MODS_CODE } from "../qweb/extensions";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
+34
-10
@@ -22,7 +22,7 @@ import { Observer } from "./core/observer";
|
|||||||
* trigger a rerendering of the current component.
|
* trigger a rerendering of the current component.
|
||||||
*/
|
*/
|
||||||
export function useState<T>(state: T): T {
|
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__;
|
const __owl__ = component.__owl__;
|
||||||
if (!__owl__.observer) {
|
if (!__owl__.observer) {
|
||||||
__owl__.observer = new Observer();
|
__owl__.observer = new Observer();
|
||||||
@@ -35,21 +35,43 @@ export function useState<T>(state: T): T {
|
|||||||
// Life cycle hooks
|
// Life cycle hooks
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
function makeLifecycleHook(method: string, reverse: boolean = false) {
|
function makeLifecycleHook(method: string, reverse: boolean = false) {
|
||||||
return function(cb) {
|
if (reverse) {
|
||||||
const component: Component<any, any> = Component._current;
|
return function(cb) {
|
||||||
if (component.__owl__[method]) {
|
const component: Component<any, any> = Component.current!;
|
||||||
const current = component.__owl__[method];
|
if (component.__owl__[method]) {
|
||||||
if (reverse) {
|
const current = component.__owl__[method];
|
||||||
component.__owl__[method] = function() {
|
component.__owl__[method] = function() {
|
||||||
current.call(component);
|
current.call(component);
|
||||||
cb.call(component);
|
cb.call(component);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
component.__owl__[method] = cb;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return function(cb) {
|
||||||
|
const component: Component<any, any> = Component.current!;
|
||||||
|
if (component.__owl__[method]) {
|
||||||
|
const current = component.__owl__[method];
|
||||||
component.__owl__[method] = function() {
|
component.__owl__[method] = function() {
|
||||||
cb.call(component);
|
cb.call(component);
|
||||||
current.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 {
|
} else {
|
||||||
component.__owl__[method] = cb;
|
component.__owl__[method] = cb;
|
||||||
}
|
}
|
||||||
@@ -61,6 +83,9 @@ export const onWillUnmount = makeLifecycleHook("willUnmountCB");
|
|||||||
export const onWillPatch = makeLifecycleHook("willPatchCB");
|
export const onWillPatch = makeLifecycleHook("willPatchCB");
|
||||||
export const onPatched = makeLifecycleHook("patchedCB", true);
|
export const onPatched = makeLifecycleHook("patchedCB", true);
|
||||||
|
|
||||||
|
export const onWillStart = makeAsyncHook("willStartCB");
|
||||||
|
export const onWillUpdateProps = makeAsyncHook("willUpdatePropsCB");
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// useRef
|
// useRef
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -75,7 +100,7 @@ interface Ref {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useRef(name: string): Ref {
|
export function useRef(name: string): Ref {
|
||||||
const __owl__ = Component._current.__owl__;
|
const __owl__ = Component.current!.__owl__;
|
||||||
return {
|
return {
|
||||||
get el(): HTMLElement | null {
|
get el(): HTMLElement | null {
|
||||||
const val = __owl__.refs && __owl__.refs[name];
|
const val = __owl__.refs && __owl__.refs[name];
|
||||||
@@ -92,13 +117,12 @@ export function useRef(name: string): Ref {
|
|||||||
// useSubEnv
|
// useSubEnv
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This hook is a simple way to let components use a sub environment. Note that
|
* 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
|
* like for all hooks, it is important that this is only called in the
|
||||||
* constructor method.
|
* constructor method.
|
||||||
*/
|
*/
|
||||||
export function useSubEnv(nextEnv) {
|
export function useSubEnv(nextEnv) {
|
||||||
const component = Component._current;
|
const component = Component.current!;
|
||||||
component.env = Object.assign(Object.create(component.env), nextEnv);
|
component.env = Object.assign(Object.create(component.env), nextEnv);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-7
@@ -7,25 +7,31 @@
|
|||||||
import { EventBus } from "./core/event_bus";
|
import { EventBus } from "./core/event_bus";
|
||||||
import { Observer } from "./core/observer";
|
import { Observer } from "./core/observer";
|
||||||
import { QWeb } from "./qweb/index";
|
import { QWeb } from "./qweb/index";
|
||||||
import { ConnectedComponent } from "./store/connected_component";
|
import * as _store from "./store";
|
||||||
import { Store } from "./store/store";
|
|
||||||
import * as _utils from "./utils";
|
import * as _utils from "./utils";
|
||||||
import * as _tags from "./tags";
|
import * as _tags from "./tags";
|
||||||
import * as _hooks from "./hooks";
|
import * as _hooks from "./hooks";
|
||||||
import { Link } from "./router/Link";
|
import * as _context from "./context";
|
||||||
import { RouteComponent } from "./router/RouteComponent";
|
import { Link } from "./router/link";
|
||||||
import { Router } from "./router/Router";
|
import { RouteComponent } from "./router/route_component";
|
||||||
|
import { Router } from "./router/router";
|
||||||
|
|
||||||
export { Component } from "./component/component";
|
export { Component } from "./component/component";
|
||||||
export { QWeb };
|
export { QWeb };
|
||||||
|
|
||||||
|
export const Context = _context.Context;
|
||||||
export const useState = _hooks.useState;
|
export const useState = _hooks.useState;
|
||||||
export const core = { EventBus, Observer };
|
export const core = { EventBus, Observer };
|
||||||
export const router = { Router, RouteComponent, Link };
|
export const router = { Router, RouteComponent, Link };
|
||||||
export const store = { Store, ConnectedComponent };
|
export const Store = _store.Store;
|
||||||
export const utils = _utils;
|
export const utils = _utils;
|
||||||
export const tags = _tags;
|
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__ = {};
|
export const __info__ = {};
|
||||||
|
|
||||||
Object.defineProperty(__info__, "mode", {
|
Object.defineProperty(__info__, "mode", {
|
||||||
|
|||||||
+10
-22
@@ -1,6 +1,7 @@
|
|||||||
import { Context } from "./context";
|
import { CompilationContext } from "./compilation_context";
|
||||||
import { QWebExprVar } from "./expression_parser";
|
import { QWebExprVar } from "./expression_parser";
|
||||||
import { QWeb } from "./qweb";
|
import { QWeb } from "./qweb";
|
||||||
|
import { htmlToVDOM } from "../vdom/html_to_vdom";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl QWeb Directives
|
* Owl QWeb Directives
|
||||||
@@ -25,7 +26,9 @@ QWeb.utils.getFragment = function(str: string): DocumentFragment {
|
|||||||
return temp.content;
|
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) {
|
if (value === "0" && ctx.caller) {
|
||||||
qweb._compileNode(ctx.caller, ctx);
|
qweb._compileNode(ctx.caller, ctx);
|
||||||
return;
|
return;
|
||||||
@@ -55,18 +58,13 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
|||||||
ctx.rootContext.rootNode = nodeID;
|
ctx.rootContext.rootNode = nodeID;
|
||||||
ctx.rootContext.parentTextNode = nodeID;
|
ctx.rootContext.parentTextNode = nodeID;
|
||||||
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
|
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
|
||||||
ctx.addLine(`result = vn${nodeID}`);
|
if (ctx.rootContext.shouldDefineResult) {
|
||||||
|
ctx.addLine(`result = vn${nodeID}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let fragID = ctx.generateID();
|
|
||||||
ctx.rootContext.shouldDefineUtils = true;
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
ctx.addLine(`var frag${fragID} = utils.getFragment(${exprID})`);
|
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${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});`);
|
|
||||||
}
|
}
|
||||||
if (node.childNodes.length) {
|
if (node.childNodes.length) {
|
||||||
ctx.addElse();
|
ctx.addElse();
|
||||||
@@ -80,11 +78,6 @@ QWeb.addDirective({
|
|||||||
name: "esc",
|
name: "esc",
|
||||||
priority: 70,
|
priority: 70,
|
||||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
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")!);
|
let value = ctx.getValue(node.getAttribute("t-esc")!);
|
||||||
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
|
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
|
||||||
return true;
|
return true;
|
||||||
@@ -95,11 +88,6 @@ QWeb.addDirective({
|
|||||||
name: "raw",
|
name: "raw",
|
||||||
priority: 80,
|
priority: 80,
|
||||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
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")!);
|
let value = ctx.getValue(node.getAttribute("t-raw")!);
|
||||||
compileValueNode(value, node, qweb, ctx);
|
compileValueNode(value, node, qweb, ctx);
|
||||||
return true;
|
return true;
|
||||||
@@ -199,7 +187,7 @@ QWeb.addDirective({
|
|||||||
nodeCopy.removeAttribute("t-call");
|
nodeCopy.removeAttribute("t-call");
|
||||||
|
|
||||||
// extract variables from nodecopy
|
// extract variables from nodecopy
|
||||||
const tempCtx = new Context();
|
const tempCtx = new CompilationContext();
|
||||||
tempCtx.nextID = ctx.rootContext.nextID;
|
tempCtx.nextID = ctx.rootContext.nextID;
|
||||||
tempCtx.allowMultipleRoots = true;
|
tempCtx.allowMultipleRoots = true;
|
||||||
qweb._compileNode(nodeCopy, tempCtx);
|
qweb._compileNode(nodeCopy, tempCtx);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const INTERP_REGEXP = /\{\{.*?\}\}/g;
|
|||||||
// Compilation Context
|
// Compilation Context
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
export class Context {
|
export class CompilationContext {
|
||||||
nextID: number = 1;
|
nextID: number = 1;
|
||||||
code: string[] = [];
|
code: string[] = [];
|
||||||
variables: { [key: string]: QWebVar } = {};
|
variables: { [key: string]: QWebVar } = {};
|
||||||
@@ -14,7 +14,7 @@ export class Context {
|
|||||||
parentTextNode: number | null = null;
|
parentTextNode: number | null = null;
|
||||||
rootNode: number | null = null;
|
rootNode: number | null = null;
|
||||||
indentLevel: number = 0;
|
indentLevel: number = 0;
|
||||||
rootContext: Context;
|
rootContext: CompilationContext;
|
||||||
caller: Element | undefined;
|
caller: Element | undefined;
|
||||||
shouldDefineOwner: boolean = false;
|
shouldDefineOwner: boolean = false;
|
||||||
shouldDefineParent: boolean = false;
|
shouldDefineParent: boolean = false;
|
||||||
@@ -87,7 +87,7 @@ export class Context {
|
|||||||
return this.code;
|
return this.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
withParent(node: number): Context {
|
withParent(node: number): CompilationContext {
|
||||||
if (
|
if (
|
||||||
!this.allowMultipleRoots &&
|
!this.allowMultipleRoots &&
|
||||||
this === this.rootContext &&
|
this === this.rootContext &&
|
||||||
@@ -98,13 +98,13 @@ export class Context {
|
|||||||
if (!this.rootContext.rootNode) {
|
if (!this.rootContext.rootNode) {
|
||||||
this.rootContext.rootNode = node;
|
this.rootContext.rootNode = node;
|
||||||
}
|
}
|
||||||
if (!this.parentNode) {
|
if (!this.parentNode && this.rootContext.shouldDefineResult) {
|
||||||
this.addLine(`result = vn${node};`);
|
this.addLine(`result = vn${node};`);
|
||||||
}
|
}
|
||||||
return this.subContext("parentNode", 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);
|
const newContext = Object.create(this);
|
||||||
newContext[key] = value;
|
newContext[key] = value;
|
||||||
return newContext;
|
return newContext;
|
||||||
+35
-13
@@ -49,25 +49,35 @@ QWeb.addDirective({
|
|||||||
);
|
);
|
||||||
ctx.closeIf();
|
ctx.closeIf();
|
||||||
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
|
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
|
||||||
let handler;
|
|
||||||
if (mods.length > 0) {
|
if (mods.length > 0) {
|
||||||
handler = `function (e) {`;
|
let handler = `function (e) {`;
|
||||||
handler += mods
|
handler += mods
|
||||||
.map(function(mod) {
|
.map(function(mod) {
|
||||||
return MODS_CODE[mod];
|
return MODS_CODE[mod];
|
||||||
})
|
})
|
||||||
.join("");
|
.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 {
|
} else {
|
||||||
handler = `context['${handlerName}'].bind(${params})`;
|
const handler = `context['${handlerName}'].bind(${params})`;
|
||||||
}
|
if (extraArgs) {
|
||||||
if (extraArgs) {
|
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
|
||||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
|
} else {
|
||||||
} else {
|
ctx.addLine(
|
||||||
ctx.addLine(
|
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
|
||||||
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
|
);
|
||||||
);
|
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
|
||||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -83,6 +93,7 @@ QWeb.addDirective({
|
|||||||
const refKey = `ref${ctx.generateID()}`;
|
const refKey = `ref${ctx.generateID()}`;
|
||||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
||||||
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
|
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
|
||||||
|
addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -196,9 +207,20 @@ QWeb.addDirective({
|
|||||||
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
|
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
|
||||||
);
|
);
|
||||||
ctx.addIf(`slot${slotKey}`);
|
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(
|
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();
|
ctx.closeIf();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-13
@@ -1,6 +1,6 @@
|
|||||||
import { EventBus } from "../core/event_bus";
|
import { EventBus } from "../core/event_bus";
|
||||||
import { h, patch, VNode } from "../vdom/index";
|
import { h, patch, VNode } from "../vdom/index";
|
||||||
import { Context } from "./context";
|
import { CompilationContext } from "./compilation_context";
|
||||||
import { shallowEqual } from "../utils";
|
import { shallowEqual } from "../utils";
|
||||||
import { addNS } from "../vdom/vdom";
|
import { addNS } from "../vdom/vdom";
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ interface Template {
|
|||||||
interface CompilationInfo {
|
interface CompilationInfo {
|
||||||
node: Element;
|
node: Element;
|
||||||
qweb: QWeb;
|
qweb: QWeb;
|
||||||
ctx: Context;
|
ctx: CompilationContext;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,8 @@ const whitespaceRE = /\s+/g;
|
|||||||
const NODE_HOOKS_PARAMS = {
|
const NODE_HOOKS_PARAMS = {
|
||||||
create: "(_, n)",
|
create: "(_, n)",
|
||||||
insert: "vn",
|
insert: "vn",
|
||||||
remove: "(vn, rm)"
|
remove: "(vn, rm)",
|
||||||
|
destroy: "()"
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Utils {
|
interface Utils {
|
||||||
@@ -103,8 +104,6 @@ const UTILS: Utils = {
|
|||||||
function parseXML(xml: string): Document {
|
function parseXML(xml: string): Document {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
|
|
||||||
// we remove comments from the xml string
|
|
||||||
xml = xml.replace(/<!--[\s\S]*?-->/g, "");
|
|
||||||
const doc = parser.parseFromString(xml, "text/xml");
|
const doc = parser.parseFromString(xml, "text/xml");
|
||||||
if (doc.getElementsByTagName("parsererror").length) {
|
if (doc.getElementsByTagName("parsererror").length) {
|
||||||
let msg = "Invalid XML in template.";
|
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 isDebug = elem.attributes.hasOwnProperty("t-debug");
|
||||||
const ctx = new Context(name);
|
const ctx = new CompilationContext(name);
|
||||||
if (elem.tagName !== "t") {
|
if (elem.tagName !== "t") {
|
||||||
ctx.shouldDefineResult = false;
|
ctx.shouldDefineResult = false;
|
||||||
}
|
}
|
||||||
@@ -409,7 +408,7 @@ export class QWeb extends EventBus {
|
|||||||
* Generate code from an xml node
|
* Generate code from an xml node
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
_compileNode(node: ChildNode, ctx: Context) {
|
_compileNode(node: ChildNode, ctx: CompilationContext) {
|
||||||
if (!(node instanceof Element)) {
|
if (!(node instanceof Element)) {
|
||||||
// this is a text node, there are no directive to apply
|
// this is a text node, there are no directive to apply
|
||||||
let text = node.textContent!;
|
let text = node.textContent!;
|
||||||
@@ -420,7 +419,11 @@ export class QWeb extends EventBus {
|
|||||||
text = text.replace(whitespaceRE, " ");
|
text = text.replace(whitespaceRE, " ");
|
||||||
}
|
}
|
||||||
if (ctx.parentNode) {
|
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) {
|
} else if (ctx.parentTextNode) {
|
||||||
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
|
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
|
||||||
} else {
|
} else {
|
||||||
@@ -449,8 +452,6 @@ export class QWeb extends EventBus {
|
|||||||
fullName: string;
|
fullName: string;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
let withHandlers = false;
|
|
||||||
|
|
||||||
// maybe this is not optimal: we iterate on all attributes here, and again
|
// maybe this is not optimal: we iterate on all attributes here, and again
|
||||||
// just after for each directive.
|
// just after for each directive.
|
||||||
for (let i = 0; i < attributes.length; i++) {
|
for (let i = 0; i < attributes.length; i++) {
|
||||||
@@ -460,11 +461,21 @@ export class QWeb extends EventBus {
|
|||||||
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
|
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
|
||||||
throw new Error(`Unknown QWeb directive: '${attrName}'`);
|
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 DIR_N = QWeb.DIRECTIVES.length;
|
||||||
const ATTR_N = attributes.length;
|
const ATTR_N = attributes.length;
|
||||||
|
let withHandlers = false;
|
||||||
for (let i = 0; i < DIR_N; i++) {
|
for (let i = 0; i < DIR_N; i++) {
|
||||||
let directive = QWeb.DIRECTIVES[i];
|
let directive = QWeb.DIRECTIVES[i];
|
||||||
let fullName;
|
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
|
// nodeType 1 is generic tag
|
||||||
if (node.nodeType !== 1) {
|
if (node.nodeType !== 1) {
|
||||||
throw new Error("unsupported node type");
|
throw new Error("unsupported node type");
|
||||||
@@ -723,7 +738,7 @@ export class QWeb extends EventBus {
|
|||||||
return nodeID;
|
return nodeID;
|
||||||
}
|
}
|
||||||
|
|
||||||
_compileChildren(node: ChildNode, ctx: Context) {
|
_compileChildren(node: ChildNode, ctx: CompilationContext) {
|
||||||
if (node.childNodes.length > 0) {
|
if (node.childNodes.length > 0) {
|
||||||
for (let child of Array.from(node.childNodes)) {
|
for (let child of Array.from(node.childNodes)) {
|
||||||
this._compileNode(child, ctx);
|
this._compileNode(child, ctx);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component } from "../component/component";
|
import { Component } from "../component/component";
|
||||||
import { xml } from "../tags";
|
import { xml } from "../tags";
|
||||||
import { Destination, RouterEnv } from "./Router";
|
import { Destination, RouterEnv } from "./router";
|
||||||
|
|
||||||
type Props = Destination;
|
type Props = Destination;
|
||||||
|
|
||||||
+142
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (context['state'].display) {
|
if (context['state'].display) {
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
@@ -99,7 +97,6 @@ exports[`animations t-transition with no delay/duration 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
result = vn1;
|
|
||||||
p1.hook = {
|
p1.hook = {
|
||||||
insert: vn => {
|
insert: vn => {
|
||||||
utils.transitionInsert(vn, 'jupiler');
|
utils.transitionInsert(vn, 'jupiler');
|
||||||
@@ -120,7 +117,6 @@ exports[`animations t-transition, on a simple node (insert) 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
result = vn1;
|
|
||||||
p1.hook = {
|
p1.hook = {
|
||||||
insert: vn => {
|
insert: vn => {
|
||||||
utils.transitionInsert(vn, 'chimay');
|
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>"`;
|
||||||
@@ -10,7 +10,6 @@ exports[`async rendering delayed component with t-asyncroot directive 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
var vn2 = h('button', p2, c2);
|
var vn2 = h('button', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -93,7 +92,6 @@ exports[`async rendering fast component with t-asyncroot directive 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
var vn2 = h('button', p2, c2);
|
var vn2 = h('button', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -176,7 +174,6 @@ exports[`async rendering t-component with t-asyncroot directive: mixed re-render
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
var vn2 = h('button', p2, c2);
|
var vn2 = h('button', p2, c2);
|
||||||
c1.push(vn2);
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
const _5 = context['state'].style;
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
const ref5 = \`child\`;
|
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}))
|
Object.assign(_2, utils.toObj({d:context['state'].d}))
|
||||||
let c3 = [], p3 = {key:3,class:_2};
|
let c3 = [], p3 = {key:3,class:_2};
|
||||||
var vn3 = h('span', p3, c3);
|
var vn3 = h('span', p3, c3);
|
||||||
result = vn3;
|
|
||||||
return 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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
const ref5 = \`child\`;
|
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':''))
|
Object.assign(_2, utils.toObj(context['state'].d?'d':''))
|
||||||
let c3 = [], p3 = {key:3,class:_2};
|
let c3 = [], p3 = {key:3,class:_2};
|
||||||
var vn3 = h('span', p3, c3);
|
var vn3 = h('span', p3, c3);
|
||||||
result = vn3;
|
|
||||||
return 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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let _5 = {a:context['state'].a,b:context['state'].b};
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (context['state'].ok) {
|
if (context['state'].ok) {
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
@@ -514,7 +504,6 @@ exports[`composition sub components with some state rendered in a loop 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = context['state'].numbers;
|
var _2 = context['state'].numbers;
|
||||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
var _3 = _4 = _2;
|
var _3 = _4 = _2;
|
||||||
@@ -573,7 +562,6 @@ exports[`composition t-component with dynamic value 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (context['state'].flag) {
|
if (context['state'].flag) {
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
@@ -744,7 +729,6 @@ exports[`other directives with t-component t-on with handler bound to argument 1
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('span', p2, c2);
|
var vn2 = h('span', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1098,7 +1074,6 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let key5 = 'key' + 'somestring';
|
let key5 = 'key' + 'somestring';
|
||||||
let def3;
|
let def3;
|
||||||
@@ -1143,7 +1118,6 @@ exports[`random stuff/miscellaneous t-on with handler bound to dynamic argument
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = context['props'].items;
|
var _2 = context['props'].items;
|
||||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
var _3 = _4 = _2;
|
var _3 = _4 = _2;
|
||||||
@@ -1199,7 +1173,6 @@ exports[`t-model directive .lazy modifier 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
var vn2 = h('input', p2, c2);
|
var vn2 = h('input', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1223,7 +1196,6 @@ exports[`t-model directive basic use, on an input 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
var vn2 = h('input', p2, c2);
|
var vn2 = h('input', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1247,7 +1219,6 @@ exports[`t-model directive basic use, on another key in component 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
var vn2 = h('input', p2, c2);
|
var vn2 = h('input', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1271,7 +1242,6 @@ exports[`t-model directive on a select 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
var vn2 = h('select', p2, c2);
|
var vn2 = h('select', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1316,7 +1286,6 @@ exports[`t-model directive on a sub state key 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
var vn2 = h('input', p2, c2);
|
var vn2 = h('input', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1340,7 +1309,6 @@ exports[`t-model directive on an input type=radio 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = 'radio';
|
var _2 = 'radio';
|
||||||
var _3 = 'one';
|
var _3 = 'one';
|
||||||
var _4 = 'One';
|
var _4 = 'One';
|
||||||
@@ -1377,7 +1345,6 @@ exports[`t-model directive on an input, type=checkbox 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = 'checkbox';
|
var _2 = 'checkbox';
|
||||||
let c3 = [], p3 = {key:3,attrs:{type: _2},on:{}};
|
let c3 = [], p3 = {key:3,attrs:{type: _2},on:{}};
|
||||||
var vn3 = h('input', p3, c3);
|
var vn3 = h('input', p3, c3);
|
||||||
@@ -1408,7 +1375,6 @@ exports[`t-slot directive can define and call slots 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1571,6 +1536,9 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
|
|||||||
create: (_, n) => {
|
create: (_, n) => {
|
||||||
context.__owl__.refs[ref11] = n.elm;
|
context.__owl__.refs[ref11] = n.elm;
|
||||||
},
|
},
|
||||||
|
destroy: () => {
|
||||||
|
delete context.__owl__.refs[ref11];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
c10.push({text: \`do something\`});
|
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;
|
var _1 = context['props'].to;
|
||||||
let c2 = [], p2 = {key:2,attrs:{href: _1}};
|
let c2 = [], p2 = {key:2,attrs:{href: _1}};
|
||||||
var vn2 = h('a', p2, c2);
|
var vn2 = h('a', p2, c2);
|
||||||
result = vn2;
|
|
||||||
const slot3 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
|
const slot3 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
|
||||||
if (slot3) {
|
if (slot3) {
|
||||||
slot3.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c2, vars: extra.vars, parent: owner}));
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('u', p2, c2);
|
var vn2 = h('u', p2, c2);
|
||||||
c1.push(vn2);
|
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;
|
var _1 = context['props'].to;
|
||||||
let c2 = [], p2 = {key:2,attrs:{href: _1}};
|
let c2 = [], p2 = {key:2,attrs:{href: _1}};
|
||||||
var vn2 = h('a', p2, c2);
|
var vn2 = h('a', p2, c2);
|
||||||
result = vn2;
|
|
||||||
const slot3 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
|
const slot3 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
|
||||||
if (slot3) {
|
if (slot3) {
|
||||||
slot3.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c2, vars: extra.vars, parent: owner}));
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('u', p2, c2);
|
var vn2 = h('u', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1813,7 +1777,6 @@ exports[`t-slot directive slots are rendered with proper context, part 4 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = 'User '+context['state'].user.name;
|
var _2 = 'User '+context['state'].user.name;
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def4;
|
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`] = `
|
exports[`top level sub widgets basic use 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||||
|
|||||||
@@ -89,9 +89,8 @@ describe("basic widget properties", () => {
|
|||||||
test("crashes if it cannot find a template", async () => {
|
test("crashes if it cannot find a template", async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
class SomeWidget extends Component<any, any> {}
|
class SomeWidget extends Component<any, any> {}
|
||||||
const widget = new SomeWidget(env);
|
|
||||||
try {
|
try {
|
||||||
await widget.mount(fixture);
|
new SomeWidget(env);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toBe('Could not find template for component "SomeWidget"');
|
expect(e.message).toBe('Could not find template for component "SomeWidget"');
|
||||||
}
|
}
|
||||||
@@ -3323,6 +3322,34 @@ describe("t-slot directive", () => {
|
|||||||
|
|
||||||
expect(fixture.innerHTML).toBe(`<a href="abc">hey</a>`);
|
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", () => {
|
describe("t-model directive", () => {
|
||||||
@@ -4294,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><b>one</b><b>one</b><b>two</b><b>two</b><b>tree</b><b>tree</b></div>"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Component, Env } from "../../src/component/component";
|
import { Component, Env } from "../../src/component/component";
|
||||||
import { makeTestFixture, makeTestEnv } from "../helpers";
|
import { makeTestFixture, makeTestEnv } from "../helpers";
|
||||||
import { QWeb } from "../../src/qweb";
|
import { QWeb } from "../../src/qweb";
|
||||||
|
import { xml } from "../../src/tags";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Setup and helpers
|
// Setup and helpers
|
||||||
@@ -31,6 +32,7 @@ describe("props validation", () => {
|
|||||||
test("validation is only done in dev mode", async () => {
|
test("validation is only done in dev mode", async () => {
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
static props = ["message"];
|
static props = ["message"];
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
QWeb.dev = true;
|
QWeb.dev = true;
|
||||||
@@ -48,6 +50,7 @@ describe("props validation", () => {
|
|||||||
test("props: list of strings", async () => {
|
test("props: list of strings", async () => {
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
static props = ["message"];
|
static props = ["message"];
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
@@ -67,6 +70,7 @@ describe("props validation", () => {
|
|||||||
|
|
||||||
for (let test of Tests) {
|
for (let test of Tests) {
|
||||||
let TestWidget = class extends Widget {
|
let TestWidget = class extends Widget {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
static props = { p: test.type };
|
static props = { p: test.type };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,6 +100,7 @@ describe("props validation", () => {
|
|||||||
|
|
||||||
for (let test of Tests) {
|
for (let test of Tests) {
|
||||||
let TestWidget = class extends Widget {
|
let TestWidget = class extends Widget {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
static props = { p: { type: test.type } };
|
static props = { p: { type: test.type } };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -115,6 +120,7 @@ describe("props validation", () => {
|
|||||||
|
|
||||||
test("can validate a prop with multiple types", async () => {
|
test("can validate a prop with multiple types", async () => {
|
||||||
let TestWidget = class extends Widget {
|
let TestWidget = class extends Widget {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
static props = { p: [String, Boolean] };
|
static props = { p: [String, Boolean] };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -130,6 +136,7 @@ describe("props validation", () => {
|
|||||||
|
|
||||||
test("can validate an optional props", async () => {
|
test("can validate an optional props", async () => {
|
||||||
let TestWidget = class extends Widget {
|
let TestWidget = class extends Widget {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
static props = { p: { type: String, optional: true } };
|
static props = { p: { type: String, optional: true } };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,6 +152,7 @@ describe("props validation", () => {
|
|||||||
|
|
||||||
test("can validate an array with given primitive type", async () => {
|
test("can validate an array with given primitive type", async () => {
|
||||||
let TestWidget = class extends Widget {
|
let TestWidget = class extends Widget {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
static props = { p: { type: Array, element: String } };
|
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 () => {
|
test("can validate an array with multiple sub element types", async () => {
|
||||||
let TestWidget = class extends Widget {
|
let TestWidget = class extends Widget {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
static props = { p: { type: Array, element: [String, Boolean] } };
|
static props = { p: { type: Array, element: [String, Boolean] } };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -180,6 +189,7 @@ describe("props validation", () => {
|
|||||||
|
|
||||||
test("can validate an object with simple shape", async () => {
|
test("can validate an object with simple shape", async () => {
|
||||||
let TestWidget = class extends Widget {
|
let TestWidget = class extends Widget {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
static props = {
|
static props = {
|
||||||
p: { type: Object, shape: { id: Number, url: String } }
|
p: { type: Object, shape: { id: Number, url: String } }
|
||||||
};
|
};
|
||||||
@@ -201,6 +211,7 @@ describe("props validation", () => {
|
|||||||
|
|
||||||
test("can validate recursively complicated prop def", async () => {
|
test("can validate recursively complicated prop def", async () => {
|
||||||
let TestWidget = class extends Widget {
|
let TestWidget = class extends Widget {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
static props = {
|
static props = {
|
||||||
p: {
|
p: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -316,6 +327,7 @@ describe("default props", () => {
|
|||||||
test("can set default values", async () => {
|
test("can set default values", async () => {
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
static defaultProps = { p: 4 };
|
static defaultProps = { p: 4 };
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const w = new TestWidget(env, {});
|
const w = new TestWidget(env, {});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
+136
-2
@@ -7,6 +7,8 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
onPatched,
|
onPatched,
|
||||||
onWillPatch,
|
onWillPatch,
|
||||||
|
onWillStart,
|
||||||
|
onWillUpdateProps,
|
||||||
useSubEnv
|
useSubEnv
|
||||||
} from "../src/hooks";
|
} from "../src/hooks";
|
||||||
import { xml } from "../src/tags";
|
import { xml } from "../src/tags";
|
||||||
@@ -78,6 +80,40 @@ describe("hooks", () => {
|
|||||||
expect(steps).toEqual(["mounted", "willunmount"]);
|
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 () => {
|
test("mounted, willUnmount, onMounted, onWillUnmount order", async () => {
|
||||||
const steps: string[] = [];
|
const steps: string[] = [];
|
||||||
function useMyHook() {
|
function useMyHook() {
|
||||||
@@ -109,6 +145,46 @@ describe("hooks", () => {
|
|||||||
expect(steps).toEqual(["comp:mounted", "hook:mounted", "hook:willunmount", "comp:willunmount"]);
|
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 () => {
|
test("two different call to mounted/willunmount should work", async () => {
|
||||||
const steps: string[] = [];
|
const steps: string[] = [];
|
||||||
function useMyHook(i) {
|
function useMyHook(i) {
|
||||||
@@ -158,6 +234,27 @@ describe("hooks", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
|
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 () => {
|
test("can use onPatched, onWillPatch", async () => {
|
||||||
const steps: string[] = [];
|
const steps: string[] = [];
|
||||||
function useMyHook() {
|
function useMyHook() {
|
||||||
@@ -346,7 +443,7 @@ describe("hooks", () => {
|
|||||||
|
|
||||||
class Parent extends Component<any, any> {
|
class Parent extends Component<any, any> {
|
||||||
static template = xml`<div><t t-esc="env.val"/><Child/></div>`;
|
static template = xml`<div><t t-esc="env.val"/><Child/></div>`;
|
||||||
static components = { Child}
|
static components = { Child };
|
||||||
constructor(env) {
|
constructor(env) {
|
||||||
super(env);
|
super(env);
|
||||||
useSubEnv({ val: 3 });
|
useSubEnv({ val: 3 });
|
||||||
@@ -354,6 +451,43 @@ describe("hooks", () => {
|
|||||||
}
|
}
|
||||||
const component = new Parent(env);
|
const component = new Parent(env);
|
||||||
await component.mount(fixture);
|
await component.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe( "<div>3<div>5</div></div>");
|
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"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ exports[`attributes class and t-attf-class with ternary operation 1`] = `
|
|||||||
var _1 = 'hello ' + (context['value']?'world':'');
|
var _1 = 'hello ' + (context['value']?'world':'');
|
||||||
let c2 = [], p2 = {key:2,attrs:{class: _1}};
|
let c2 = [], p2 = {key:2,attrs:{class: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return vn2;
|
return vn2;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -19,7 +18,6 @@ exports[`attributes dynamic attribute falsy variable 1`] = `
|
|||||||
var _1 = context['value'];
|
var _1 = context['value'];
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return vn2;
|
return vn2;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -31,7 +29,6 @@ exports[`attributes dynamic attribute with a dash 1`] = `
|
|||||||
var _1 = context['id'];
|
var _1 = context['id'];
|
||||||
let c2 = [], p2 = {key:2,attrs:{\\"data-action-id\\": _1}};
|
let c2 = [], p2 = {key:2,attrs:{\\"data-action-id\\": _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return vn2;
|
return vn2;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -43,7 +40,6 @@ exports[`attributes dynamic attributes 1`] = `
|
|||||||
var _1 = 'bar';
|
var _1 = 'bar';
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return vn2;
|
return vn2;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -55,7 +51,6 @@ exports[`attributes dynamic formatted attributes with a dash 1`] = `
|
|||||||
var _1 = \`Some text \${context['id']}\`;
|
var _1 = \`Some text \${context['id']}\`;
|
||||||
let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
|
let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return vn2;
|
return vn2;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -67,7 +62,6 @@ exports[`attributes fixed variable 1`] = `
|
|||||||
var _1 = context['value'];
|
var _1 = context['value'];
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return vn2;
|
return vn2;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -79,7 +73,6 @@ exports[`attributes format expression 1`] = `
|
|||||||
var _1 = (context['value']+37);
|
var _1 = (context['value']+37);
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return vn2;
|
return vn2;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -91,7 +84,6 @@ exports[`attributes format expression, other format 1`] = `
|
|||||||
var _1 = (context['value']+37);
|
var _1 = (context['value']+37);
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return vn2;
|
return vn2;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -103,7 +95,6 @@ exports[`attributes format literal 1`] = `
|
|||||||
var _1 = \`bar\`;
|
var _1 = \`bar\`;
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return vn2;
|
return vn2;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -115,7 +106,6 @@ exports[`attributes format multiple 1`] = `
|
|||||||
var _1 = \`a \${context['value1']} is \${context['value2']} of \${context['value3']} ]\`;
|
var _1 = \`a \${context['value1']} is \${context['value2']} of \${context['value3']} ]\`;
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return vn2;
|
return vn2;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -127,7 +117,6 @@ exports[`attributes format value 1`] = `
|
|||||||
var _1 = \`b\${context['value']}r\`;
|
var _1 = \`b\${context['value']}r\`;
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return vn2;
|
return vn2;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -139,7 +128,6 @@ exports[`attributes from object variables set previously 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = {a:'b'};
|
var _2 = {a:'b'};
|
||||||
let _3 = utils.toObj(_2.a);
|
let _3 = utils.toObj(_2.a);
|
||||||
let c4 = [], p4 = {key:4,class:_3};
|
let c4 = [], p4 = {key:4,class:_3};
|
||||||
@@ -156,7 +144,6 @@ exports[`attributes from variables set previously 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = 'def';
|
var _2 = 'def';
|
||||||
let _3 = utils.toObj(_2);
|
let _3 = utils.toObj(_2);
|
||||||
let c4 = [], p4 = {key:4,class:_3};
|
let c4 = [], p4 = {key:4,class:_3};
|
||||||
@@ -180,7 +167,6 @@ exports[`attributes object 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return vn2;
|
return vn2;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -194,7 +180,6 @@ exports[`attributes static attributes 1`] = `
|
|||||||
var _3 = 'c';
|
var _3 = 'c';
|
||||||
let c4 = [], p4 = {key:4,attrs:{foo: _1,bar: _2,baz: _3}};
|
let c4 = [], p4 = {key:4,attrs:{foo: _1,bar: _2,baz: _3}};
|
||||||
var vn4 = h('div', p4, c4);
|
var vn4 = h('div', p4, c4);
|
||||||
result = vn4;
|
|
||||||
return vn4;
|
return vn4;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -207,7 +192,6 @@ exports[`attributes static attributes on void elements 1`] = `
|
|||||||
var _2 = 'Test';
|
var _2 = 'Test';
|
||||||
let c3 = [], p3 = {key:3,attrs:{src: _1,alt: _2}};
|
let c3 = [], p3 = {key:3,attrs:{src: _1,alt: _2}};
|
||||||
var vn3 = h('img', p3, c3);
|
var vn3 = h('img', p3, c3);
|
||||||
result = vn3;
|
|
||||||
return vn3;
|
return vn3;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -219,7 +203,6 @@ exports[`attributes static attributes with dashes 1`] = `
|
|||||||
var _1 = 'Close';
|
var _1 = 'Close';
|
||||||
let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
|
let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return 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']))
|
Object.assign(_2, utils.toObj(context['value']))
|
||||||
let c3 = [], p3 = {key:3,class:_2};
|
let c3 = [], p3 = {key:3,class:_2};
|
||||||
var vn3 = h('div', p3, c3);
|
var vn3 = h('div', p3, c3);
|
||||||
result = vn3;
|
|
||||||
return 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']}))
|
Object.assign(_2, utils.toObj({a:context['b'],c:context['d'],e:context['f']}))
|
||||||
let c3 = [], p3 = {key:3,class:_2};
|
let c3 = [], p3 = {key:3,class:_2};
|
||||||
var vn3 = h('div', p3, c3);
|
var vn3 = h('div', p3, c3);
|
||||||
result = vn3;
|
|
||||||
return vn3;
|
return vn3;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -259,7 +240,6 @@ exports[`attributes t-attf-class should combine with class 1`] = `
|
|||||||
var _1 = 'hello ' + \`world\`;
|
var _1 = 'hello ' + \`world\`;
|
||||||
let c2 = [], p2 = {key:2,attrs:{class: _1}};
|
let c2 = [], p2 = {key:2,attrs:{class: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return vn2;
|
return vn2;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -278,7 +258,6 @@ exports[`attributes tuple literal 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return vn2;
|
return vn2;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -297,7 +276,6 @@ exports[`attributes tuple variable 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
result = vn2;
|
|
||||||
return vn2;
|
return vn2;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -309,7 +287,6 @@ exports[`debugging t-debug 1`] = `
|
|||||||
debugger;
|
debugger;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (true) {
|
if (true) {
|
||||||
debugger;
|
debugger;
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
@@ -327,7 +304,6 @@ exports[`debugging t-log 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = 42;
|
var _2 = 42;
|
||||||
console.log(_2+3)
|
console.log(_2+3)
|
||||||
return vn1;
|
return vn1;
|
||||||
@@ -341,7 +317,6 @@ exports[`foreach does not pollute the rendering context 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = [1];
|
var _2 = [1];
|
||||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
var _3 = _4 = _2;
|
var _3 = _4 = _2;
|
||||||
@@ -372,7 +347,6 @@ exports[`foreach iterate on items (on a element node) 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = [1,2];
|
var _2 = [1,2];
|
||||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
var _3 = _4 = _2;
|
var _3 = _4 = _2;
|
||||||
@@ -407,7 +381,6 @@ exports[`foreach iterate on items 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = [3,2,1];
|
var _2 = [3,2,1];
|
||||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
var _3 = _4 = _2;
|
var _3 = _4 = _2;
|
||||||
@@ -450,7 +423,6 @@ exports[`foreach iterate, dict param 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = context['value'];
|
var _2 = context['value'];
|
||||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
var _3 = _4 = _2;
|
var _3 = _4 = _2;
|
||||||
@@ -493,7 +465,6 @@ exports[`foreach iterate, position 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = Array(5);
|
var _2 = Array(5);
|
||||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
var _3 = _4 = _2;
|
var _3 = _4 = _2;
|
||||||
@@ -533,7 +504,6 @@ exports[`foreach warn if no key in some case 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = [1,2];
|
var _2 = [1,2];
|
||||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
var _3 = _4 = _2;
|
var _3 = _4 = _2;
|
||||||
@@ -566,7 +536,6 @@ exports[`loading templates can initialize qweb with a string 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
c1.push({text: \`jupiler\`});
|
c1.push({text: \`jupiler\`});
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -578,7 +547,6 @@ exports[`loading templates can load a few templates from a xml string 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('ul', p1, c1);
|
var vn1 = h('ul', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('li', p2, c2);
|
var vn2 = h('li', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -599,7 +567,6 @@ exports[`misc global 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = [4,5,6];
|
var _2 = [4,5,6];
|
||||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
var _3 = _4 = _2;
|
var _3 = _4 = _2;
|
||||||
@@ -614,7 +581,7 @@ exports[`misc global 1`] = `
|
|||||||
context.value_index = i;
|
context.value_index = i;
|
||||||
context.value = _3[i];
|
context.value = _3[i];
|
||||||
context.value_value = _4[i];
|
context.value_value = _4[i];
|
||||||
let c5 = [], p5 = {key:5,on:{}};
|
let c5 = [], p5 = {key:5};
|
||||||
var vn5 = h('span', p5, c5);
|
var vn5 = h('span', p5, c5);
|
||||||
c1.push(vn5);
|
c1.push(vn5);
|
||||||
var _6 = context['value'];
|
var _6 = context['value'];
|
||||||
@@ -624,12 +591,12 @@ exports[`misc global 1`] = `
|
|||||||
{
|
{
|
||||||
let _11 = 'bbb';
|
let _11 = 'bbb';
|
||||||
var _13 = 'agüero';
|
var _13 = 'agüero';
|
||||||
let c14 = [], p14 = {key:14,attrs:{\\"falló\\": _13},on:{}};
|
let c14 = [], p14 = {key:14,attrs:{\\"falló\\": _13}};
|
||||||
var vn14 = h('Año', p14, c14);
|
var vn14 = h('año', p14, c14);
|
||||||
c1.push(vn14);
|
c1.push(vn14);
|
||||||
{
|
{
|
||||||
let _15 = 'aaa';
|
let _15 = 'aaa';
|
||||||
let c16 = [], p16 = {key:16,on:{}};
|
let c16 = [], p16 = {key:16};
|
||||||
var vn16 = h('span', p16, c16);
|
var vn16 = h('span', p16, c16);
|
||||||
c14.push(vn16);
|
c14.push(vn16);
|
||||||
if (_15 || _15 === 0) {
|
if (_15 || _15 === 0) {
|
||||||
@@ -638,7 +605,7 @@ exports[`misc global 1`] = `
|
|||||||
c16.push({text: \`foo default\`});
|
c16.push({text: \`foo default\`});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let c17 = [], p17 = {key:17,on:{}};
|
let c17 = [], p17 = {key:17};
|
||||||
var vn17 = h('span', p17, c17);
|
var vn17 = h('span', p17, c17);
|
||||||
c14.push(vn17);
|
c14.push(vn17);
|
||||||
var _18 = context['foo'];
|
var _18 = context['foo'];
|
||||||
@@ -648,7 +615,7 @@ exports[`misc global 1`] = `
|
|||||||
c17.push({text: \`foo default\`});
|
c17.push({text: \`foo default\`});
|
||||||
}
|
}
|
||||||
var _19 = 'bbb';
|
var _19 = 'bbb';
|
||||||
let c20 = [], p20 = {key:20,on:{}};
|
let c20 = [], p20 = {key:20};
|
||||||
var vn20 = h('span', p20, c20);
|
var vn20 = h('span', p20, c20);
|
||||||
c14.push(vn20);
|
c14.push(vn20);
|
||||||
if (_19 || _19 === 0) {
|
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);
|
var vn21 = h('div', p21, c21);
|
||||||
c1.push(vn21);
|
c1.push(vn21);
|
||||||
var _22 = context['toto'];
|
var _22 = context['toto'];
|
||||||
if (_22 || _22 === 0) {
|
if (_22 || _22 === 0) {
|
||||||
var frag23 = utils.getFragment(_22)
|
c21.push(...utils.htmlToVDOM(_22));
|
||||||
var p24 = {hook: {
|
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag23, n.elm),
|
|
||||||
}};
|
|
||||||
var vn24 = h('div', p24)
|
|
||||||
c21.push(vn24);
|
|
||||||
} else {
|
} else {
|
||||||
c21.push({text: \`toto default\`});
|
c21.push({text: \`toto default\`});
|
||||||
}
|
}
|
||||||
@@ -683,7 +645,6 @@ exports[`properly support svg add proper namespace to g tags 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('g', p1, c1);
|
var vn1 = h('g', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = '50';
|
var _2 = '50';
|
||||||
var _3 = '50';
|
var _3 = '50';
|
||||||
var _4 = '4';
|
var _4 = '4';
|
||||||
@@ -708,7 +669,6 @@ exports[`properly support svg add proper namespace to svg 1`] = `
|
|||||||
var _2 = '90px';
|
var _2 = '90px';
|
||||||
let c3 = [], p3 = {key:3,attrs:{width: _1,height: _2}};
|
let c3 = [], p3 = {key:3,attrs:{width: _1,height: _2}};
|
||||||
var vn3 = h('svg', p3, c3);
|
var vn3 = h('svg', p3, c3);
|
||||||
result = vn3;
|
|
||||||
var _4 = '50';
|
var _4 = '50';
|
||||||
var _5 = '50';
|
var _5 = '50';
|
||||||
var _6 = '4';
|
var _6 = '4';
|
||||||
@@ -732,7 +692,6 @@ exports[`special cases for some boolean html attributes/properties input type= c
|
|||||||
var _2 = context['flag'];
|
var _2 = context['flag'];
|
||||||
let c3 = [], p3 = {key:3,attrs:{type: _1,checked: _2},props:{checked: _2}};
|
let c3 = [], p3 = {key:3,attrs:{type: _1,checked: _2},props:{checked: _2}};
|
||||||
var vn3 = h('input', p3, c3);
|
var vn3 = h('input', p3, c3);
|
||||||
result = vn3;
|
|
||||||
return vn3;
|
return vn3;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -743,7 +702,6 @@ exports[`special cases for some boolean html attributes/properties various boole
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = 'checkbox';
|
var _2 = 'checkbox';
|
||||||
var _3 = 'checked';
|
var _3 = 'checked';
|
||||||
let c4 = [], p4 = {key:4,attrs:{type: _2,checked: _3},props:{checked: _3}};
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('span', p2, c2);
|
var vn2 = h('span', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -799,7 +756,6 @@ exports[`static templates div with a text node 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
c1.push({text: \`word\`});
|
c1.push({text: \`word\`});
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -811,19 +767,19 @@ exports[`static templates empty div 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`static templates ignore comments 1`] = `
|
exports[`static templates properly handle comments 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
c1.push({text: \`hello \`});
|
||||||
c1.push({text: \`hello owl\`});
|
c1.push(h('!', \` comment\`));
|
||||||
|
c1.push({text: \`owl\`});
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -887,7 +843,6 @@ exports[`t-call (template calling call with several sub nodes on same line 1`] =
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c5 = [], p5 = {key:5};
|
let c5 = [], p5 = {key:5};
|
||||||
var vn5 = h('div', p5, c5);
|
var vn5 = h('div', p5, c5);
|
||||||
c1.push(vn5);
|
c1.push(vn5);
|
||||||
@@ -910,7 +865,6 @@ exports[`t-call (template calling inherit context 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = 1;
|
var _2 = 1;
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
c1.push({text: _2});
|
c1.push({text: _2});
|
||||||
@@ -926,7 +880,6 @@ exports[`t-call (template calling recursive template, part 1 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('span', p2, c2);
|
var vn2 = h('span', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -967,7 +920,6 @@ exports[`t-call (template calling recursive template, part 2 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
{
|
{
|
||||||
let _2 = context['root'];
|
let _2 = context['root'];
|
||||||
let c3 = [], p3 = {key:3};
|
let c3 = [], p3 = {key:3};
|
||||||
@@ -1059,7 +1011,6 @@ exports[`t-call (template calling recursive template, part 3 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
{
|
{
|
||||||
let _2 = context['root'];
|
let _2 = context['root'];
|
||||||
let c3 = [], p3 = {key:3};
|
let c3 = [], p3 = {key:3};
|
||||||
@@ -1148,7 +1099,6 @@ exports[`t-call (template calling scoped parameters 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
{
|
{
|
||||||
let _2 = 42;
|
let _2 = 42;
|
||||||
c1.push({text: \`ok\`});
|
c1.push({text: \`ok\`});
|
||||||
@@ -1167,7 +1117,6 @@ exports[`t-call (template calling t-call with t-if 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (context['flag']) {
|
if (context['flag']) {
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('span', p2, c2);
|
var vn2 = h('span', p2, c2);
|
||||||
@@ -1184,7 +1133,6 @@ exports[`t-call (template calling t-call, global templates 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('span', p2, c2);
|
var vn2 = h('span', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1241,7 +1189,6 @@ exports[`t-call (template calling with used set body 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
result = vn1;
|
|
||||||
{
|
{
|
||||||
let _2 = 'ok';
|
let _2 = 'ok';
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
@@ -1256,9 +1203,8 @@ exports[`t-esc escaping on a node 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = 'ok';
|
var _2 = 'ok';
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
c1.push({text: _2});
|
c1.push({text: _2});
|
||||||
@@ -1271,9 +1217,8 @@ exports[`t-esc escaping on a node with a body 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = 'ok';
|
var _2 = 'ok';
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
c1.push({text: _2});
|
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
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = context['var'];
|
var _2 = context['var'];
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
c1.push({text: _2});
|
c1.push({text: _2});
|
||||||
@@ -1307,7 +1251,6 @@ exports[`t-esc literal 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = 'ok';
|
var _2 = 'ok';
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
c1.push({text: _2});
|
c1.push({text: _2});
|
||||||
@@ -1322,7 +1265,6 @@ exports[`t-esc variable 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = context['var'];
|
var _2 = context['var'];
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
c1.push({text: _2});
|
c1.push({text: _2});
|
||||||
@@ -1337,7 +1279,6 @@ exports[`t-if boolean value condition elif 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (context['color']=='black') {
|
if (context['color']=='black') {
|
||||||
c1.push({text: \`black pearl\`});
|
c1.push({text: \`black pearl\`});
|
||||||
}
|
}
|
||||||
@@ -1360,7 +1301,6 @@ exports[`t-if boolean value condition else 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('span', p2, c2);
|
var vn2 = h('span', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1385,7 +1325,6 @@ exports[`t-if boolean value condition false else 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('span', p2, c2);
|
var vn2 = h('span', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1410,7 +1349,6 @@ exports[`t-if boolean value condition missing 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (context['condition']) {
|
if (context['condition']) {
|
||||||
c1.push({text: \`fail\`});
|
c1.push({text: \`fail\`});
|
||||||
}
|
}
|
||||||
@@ -1424,7 +1362,6 @@ exports[`t-if boolean value false condition 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (context['condition']) {
|
if (context['condition']) {
|
||||||
c1.push({text: \`ok\`});
|
c1.push({text: \`ok\`});
|
||||||
}
|
}
|
||||||
@@ -1438,7 +1375,6 @@ exports[`t-if boolean value true condition 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (context['condition']) {
|
if (context['condition']) {
|
||||||
c1.push({text: \`ok\`});
|
c1.push({text: \`ok\`});
|
||||||
}
|
}
|
||||||
@@ -1452,7 +1388,6 @@ exports[`t-if can use some boolean operators in expressions 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (context['cond1']&&context['cond2']) {
|
if (context['cond1']&&context['cond2']) {
|
||||||
c1.push({text: \`and\`});
|
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
|
const nodeKey1 = context['beer'].id
|
||||||
let c1 = [], p1 = {key:nodeKey1};
|
let c1 = [], p1 = {key:nodeKey1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = context['beer'].name;
|
var _2 = context['beer'].name;
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
c1.push({text: _2});
|
c1.push({text: _2});
|
||||||
@@ -1504,7 +1438,6 @@ exports[`t-key t-key directive in a list 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('ul', p1, c1);
|
var vn1 = h('ul', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = context['beers'];
|
var _2 = context['beers'];
|
||||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
var _3 = _4 = _2;
|
var _3 = _4 = _2;
|
||||||
@@ -1541,7 +1474,6 @@ exports[`t-on can bind event handler 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('button', p1, c1);
|
var vn1 = h('button', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (!context['add']) {
|
if (!context['add']) {
|
||||||
throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('button', p1, c1);
|
var vn1 = h('button', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (!context['add']) {
|
if (!context['add']) {
|
||||||
throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('button', p1, c1);
|
var vn1 = h('button', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (!context['doSomething']) {
|
if (!context['doSomething']) {
|
||||||
throw new Error('Missing handler \\\\'' + 'doSomething' + \`\\\\' when evaluating template 'test'\`)
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('button', p1, c1);
|
var vn1 = h('button', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (!context['doSomething']) {
|
if (!context['doSomething']) {
|
||||||
throw new Error('Missing handler \\\\'' + 'doSomething' + \`\\\\' when evaluating template 'test'\`)
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('ul', p1, c1);
|
var vn1 = h('ul', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = ['someval'];
|
var _2 = ['someval'];
|
||||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
var _3 = _4 = _2;
|
var _3 = _4 = _2;
|
||||||
@@ -1650,7 +1578,6 @@ exports[`t-on can bind handlers with object arguments 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('button', p1, c1);
|
var vn1 = h('button', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (!context['add']) {
|
if (!context['add']) {
|
||||||
throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('button', p1, c1);
|
var vn1 = h('button', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (!context['handleClick']) {
|
if (!context['handleClick']) {
|
||||||
throw new Error('Missing handler \\\\'' + 'handleClick' + \`\\\\' when evaluating template 'test'\`)
|
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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('button', p1, c1);
|
var vn1 = h('button', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (!context['add']) {
|
if (!context['add']) {
|
||||||
throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)
|
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`] = `
|
exports[`t-on t-on with prevent and self modifiers (order matters) 1`] = `
|
||||||
"function anonymous(context,extra
|
"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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
var vn2 = h('button', p2, c2);
|
var vn2 = h('button', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1732,7 +1703,6 @@ exports[`t-on t-on with prevent and/or stop modifiers 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
var vn2 = h('button', p2, c2);
|
var vn2 = h('button', p2, c2);
|
||||||
c1.push(vn2);
|
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`] = `
|
exports[`t-on t-on with self and prevent modifiers (order matters) 1`] = `
|
||||||
"function anonymous(context,extra
|
"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;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
var vn2 = h('button', p2, c2);
|
var vn2 = h('button', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1795,7 +1806,6 @@ exports[`t-on t-on with self modifier 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
var vn2 = h('button', p2, c2);
|
var vn2 = h('button', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1831,15 +1841,9 @@ exports[`t-raw literal 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = 'ok';
|
var _2 = 'ok';
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
var frag3 = utils.getFragment(_2)
|
c1.push(...utils.htmlToVDOM(_2));
|
||||||
var p4 = {hook: {
|
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
|
|
||||||
}};
|
|
||||||
var vn4 = h('div', p4)
|
|
||||||
c1.push(vn4);
|
|
||||||
}
|
}
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -1852,15 +1856,9 @@ exports[`t-raw not escaping 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = context['var'];
|
var _2 = context['var'];
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
var frag3 = utils.getFragment(_2)
|
c1.push(...utils.htmlToVDOM(_2));
|
||||||
var p4 = {hook: {
|
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
|
|
||||||
}};
|
|
||||||
var vn4 = h('div', p4)
|
|
||||||
c1.push(vn4);
|
|
||||||
}
|
}
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -1873,19 +1871,13 @@ exports[`t-raw t-raw and another sibling node 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('span', p2, c2);
|
var vn2 = h('span', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
c2.push({text: \`hello\`});
|
c2.push({text: \`hello\`});
|
||||||
var _3 = context['var'];
|
var _3 = context['var'];
|
||||||
if (_3 || _3 === 0) {
|
if (_3 || _3 === 0) {
|
||||||
var frag4 = utils.getFragment(_3)
|
c1.push(...utils.htmlToVDOM(_3));
|
||||||
var p5 = {hook: {
|
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag4, n.elm),
|
|
||||||
}};
|
|
||||||
var vn5 = h('div', p5)
|
|
||||||
c1.push(vn5);
|
|
||||||
}
|
}
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -1898,15 +1890,9 @@ exports[`t-raw variable 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = context['var'];
|
var _2 = context['var'];
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
var frag3 = utils.getFragment(_2)
|
c1.push(...utils.htmlToVDOM(_2));
|
||||||
var p4 = {hook: {
|
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
|
|
||||||
}};
|
|
||||||
var vn4 = h('div', p4)
|
|
||||||
c1.push(vn4);
|
|
||||||
}
|
}
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -1919,7 +1905,6 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('span', p2, c2);
|
var vn2 = h('span', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1928,6 +1913,9 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
|
|||||||
create: (_, n) => {
|
create: (_, n) => {
|
||||||
context.__owl__.refs[ref3] = n.elm;
|
context.__owl__.refs[ref3] = n.elm;
|
||||||
},
|
},
|
||||||
|
destroy: () => {
|
||||||
|
delete context.__owl__.refs[ref3];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -1940,7 +1928,6 @@ exports[`t-ref can get a ref on a node 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('span', p2, c2);
|
var vn2 = h('span', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
@@ -1949,6 +1936,9 @@ exports[`t-ref can get a ref on a node 1`] = `
|
|||||||
create: (_, n) => {
|
create: (_, n) => {
|
||||||
context.__owl__.refs[ref3] = n.elm;
|
context.__owl__.refs[ref3] = n.elm;
|
||||||
},
|
},
|
||||||
|
destroy: () => {
|
||||||
|
delete context.__owl__.refs[ref3];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -1962,7 +1952,6 @@ exports[`t-ref refs in a loop 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = context['items'];
|
var _2 = context['items'];
|
||||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
var _3 = _4 = _2;
|
var _3 = _4 = _2;
|
||||||
@@ -1986,6 +1975,9 @@ exports[`t-ref refs in a loop 1`] = `
|
|||||||
create: (_, n) => {
|
create: (_, n) => {
|
||||||
context.__owl__.refs[ref6] = n.elm;
|
context.__owl__.refs[ref6] = n.elm;
|
||||||
},
|
},
|
||||||
|
destroy: () => {
|
||||||
|
delete context.__owl__.refs[ref6];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
var _7 = context['item'];
|
var _7 = context['item'];
|
||||||
if (_7 || _7 === 0) {
|
if (_7 || _7 === 0) {
|
||||||
@@ -2002,7 +1994,6 @@ exports[`t-set evaluate value expression 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = 1+2;
|
var _2 = 1+2;
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
c1.push({text: _2});
|
c1.push({text: _2});
|
||||||
@@ -2017,7 +2008,6 @@ exports[`t-set evaluate value expression, part 2 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = context['somevariable']+2;
|
var _2 = context['somevariable']+2;
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
c1.push({text: _2});
|
c1.push({text: _2});
|
||||||
@@ -2032,7 +2022,6 @@ exports[`t-set set from attribute literal 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = 'ok';
|
var _2 = 'ok';
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
c1.push({text: _2});
|
c1.push({text: _2});
|
||||||
@@ -2047,7 +2036,6 @@ exports[`t-set set from attribute lookup 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = context['value'];
|
var _2 = context['value'];
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
c1.push({text: _2});
|
c1.push({text: _2});
|
||||||
@@ -2073,7 +2061,6 @@ exports[`t-set set from body lookup 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = context['value'];
|
var _2 = context['value'];
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
c1.push({text: _2});
|
c1.push({text: _2});
|
||||||
@@ -2088,7 +2075,6 @@ exports[`t-set set from empty body 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -2099,7 +2085,6 @@ exports[`t-set t-set and t-if 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = context['value'];
|
var _2 = context['value'];
|
||||||
if (_2==='ok') {
|
if (_2==='ok') {
|
||||||
c1.push({text: \`grimbergen\`});
|
c1.push({text: \`grimbergen\`});
|
||||||
@@ -2114,7 +2099,6 @@ exports[`t-set t-set evaluates an expression only once 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = context['value']+' artois';
|
var _2 = context['value']+' artois';
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
c1.push({text: _2});
|
c1.push({text: _2});
|
||||||
@@ -2133,7 +2117,6 @@ exports[`t-set t-set should reuse variable if possible 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = 1;
|
var _2 = 1;
|
||||||
var _3 = context['list'];
|
var _3 = context['list'];
|
||||||
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
|
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
@@ -2172,7 +2155,6 @@ exports[`t-set value priority 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
var _2 = 1;
|
var _2 = 1;
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
c1.push({text: _2});
|
c1.push({text: _2});
|
||||||
@@ -2187,7 +2169,6 @@ exports[`whitespace handling consecutives whitespaces are condensed into a singl
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
c1.push({text: \` abc \`});
|
c1.push({text: \` abc \`});
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -2199,7 +2180,6 @@ exports[`whitespace handling nothing is done in pre tags 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('pre', p1, c1);
|
var vn1 = h('pre', p1, c1);
|
||||||
result = vn1;
|
|
||||||
c1.push({text: \` \`});
|
c1.push({text: \` \`});
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -2211,7 +2191,6 @@ exports[`whitespace handling nothing is done in pre tags 2`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('pre', p1, c1);
|
var vn1 = h('pre', p1, c1);
|
||||||
result = vn1;
|
|
||||||
c1.push({text: \`
|
c1.push({text: \`
|
||||||
some text
|
some text
|
||||||
\`});
|
\`});
|
||||||
@@ -2225,7 +2204,6 @@ exports[`whitespace handling nothing is done in pre tags 3`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('pre', p1, c1);
|
var vn1 = h('pre', p1, c1);
|
||||||
result = vn1;
|
|
||||||
c1.push({text: \`
|
c1.push({text: \`
|
||||||
|
|
||||||
\`});
|
\`});
|
||||||
@@ -2239,7 +2217,6 @@ exports[`whitespace handling white space only text nodes are condensed into a si
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
c1.push({text: \` \`});
|
c1.push({text: \` \`});
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -2251,7 +2228,6 @@ exports[`whitespace handling whitespace only text nodes with newlines are remove
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('span', p2, c2);
|
var vn2 = h('span', p2, c2);
|
||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
|
|||||||
+75
-3
@@ -50,9 +50,9 @@ describe("static templates", () => {
|
|||||||
expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>");
|
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>");
|
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", () => {
|
describe("misc", () => {
|
||||||
test("global", () => {
|
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-uses-foo", `<span t-esc="foo">foo default</span>`);
|
||||||
qweb.addTemplate("_callee-asc-toto", `<div t-raw="toto">toto default</div>`);
|
qweb.addTemplate("_callee-asc-toto", `<div t-raw="toto">toto default</div>`);
|
||||||
qweb.addTemplate(
|
qweb.addTemplate(
|
||||||
@@ -1086,6 +1086,78 @@ describe("t-on", () => {
|
|||||||
|
|
||||||
expect(steps).toEqual([true, true]);
|
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", () => {
|
describe("t-ref", () => {
|
||||||
|
|||||||
-1
@@ -10,7 +10,6 @@ exports[`Link component can render simple cases 1`] = `
|
|||||||
var _2 = context['href'];
|
var _2 = context['href'];
|
||||||
let c3 = [], p3 = {key:3,attrs:{href: _2},class:_1,on:{}};
|
let c3 = [], p3 = {key:3,attrs:{href: _2},class:_1,on:{}};
|
||||||
var vn3 = h('a', p3, c3);
|
var vn3 = h('a', p3, c3);
|
||||||
result = vn3;
|
|
||||||
if (!context['navigate']) {
|
if (!context['navigate']) {
|
||||||
throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__template__1'\`)
|
throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__template__1'\`)
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Component } from "../../src/component/component";
|
import { Component } from "../../src/component/component";
|
||||||
import { Link } from "../../src/router/Link";
|
import { Link } from "../../src/router/link";
|
||||||
import { RouterEnv } from "../../src/router/Router";
|
import { RouterEnv } from "../../src/router/router";
|
||||||
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
||||||
import { TestRouter } from "./TestRouter";
|
import { TestRouter } from "./test_router";
|
||||||
|
|
||||||
describe("Link component", () => {
|
describe("Link component", () => {
|
||||||
let fixture: HTMLElement;
|
let fixture: HTMLElement;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Component } from "../../src/component/component";
|
import { Component } from "../../src/component/component";
|
||||||
import { RouterEnv } from "../../src/router/Router";
|
import { RouterEnv } from "../../src/router/router";
|
||||||
import { RouteComponent } from "../../src/router/RouteComponent";
|
import { RouteComponent } from "../../src/router/route_component";
|
||||||
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
||||||
import { TestRouter } from "./TestRouter";
|
import { TestRouter } from "./test_router";
|
||||||
|
|
||||||
describe("RouteComponent", () => {
|
describe("RouteComponent", () => {
|
||||||
let fixture: HTMLElement;
|
let fixture: HTMLElement;
|
||||||
@@ -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 { makeTestEnv, nextTick } from "../helpers";
|
||||||
import { TestRouter } from "./TestRouter";
|
import { TestRouter } from "./test_router";
|
||||||
|
|
||||||
let env: RouterEnv;
|
let env: RouterEnv;
|
||||||
let router: TestRouter | null = null;
|
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";
|
import { QWeb } from "../../src/qweb/index";
|
||||||
|
|
||||||
export class TestRouter extends Router {
|
export class TestRouter extends Router {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Env } from "../../src/component/component";
|
import { Env } from "../src/component/component";
|
||||||
import { Store, Getter } from "../../src/store/store";
|
import { Store, Getter } from "../src/store";
|
||||||
import { nextTick, nextMicroTick } from "../helpers";
|
import { nextTick, nextMicroTick } from "./helpers";
|
||||||
|
|
||||||
describe("basic use", () => {
|
describe("basic use", () => {
|
||||||
test("dispatch an action", () => {
|
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
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { h, patch } from "../src/vdom";
|
import { h, patch } from "../src/vdom";
|
||||||
|
import { htmlToVDOM } from "../src/vdom/html_to_vdom";
|
||||||
import { init, addNS } from "../src/vdom/vdom";
|
import { init, addNS } from "../src/vdom/vdom";
|
||||||
|
|
||||||
function map(list, fn) {
|
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
@@ -1,5 +1,5 @@
|
|||||||
import { SAMPLES } from "./samples.js";
|
import { SAMPLES } from "./samples.js";
|
||||||
const {useState, useRef} = owl.hooks;
|
const { useState, useRef, onMounted, onWillUnmount } = owl.hooks;
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Constants, helpers, utils
|
// Constants, helpers, utils
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -150,6 +150,51 @@ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start);
|
|||||||
return zip.generateAsync({ type: "blob" });
|
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
|
// Tabbed editor
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -157,13 +202,14 @@ class TabbedEditor extends owl.Component {
|
|||||||
constructor(parent, props) {
|
constructor(parent, props) {
|
||||||
super(parent, props);
|
super(parent, props);
|
||||||
this.state = useState({
|
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.setTab = owl.utils.debounce(this.setTab, 250, true);
|
||||||
|
|
||||||
this.sessions = {};
|
this.sessions = {};
|
||||||
this._setupSessions(props);
|
this._setupSessions(props);
|
||||||
this.editorNode = useRef("editor");
|
this.editorNode = useRef("editor");
|
||||||
|
this._updateCode = this._updateCode.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -175,18 +221,14 @@ class TabbedEditor extends owl.Component {
|
|||||||
this.editor.setSession(this.sessions[this.state.currentTab]);
|
this.editor.setSession(this.sessions[this.state.currentTab]);
|
||||||
const tabSize = this.state.currentTab === "xml" ? 2 : 4;
|
const tabSize = this.state.currentTab === "xml" ? 2 : 4;
|
||||||
this.editor.session.setOption("tabSize", tabSize);
|
this.editor.session.setOption("tabSize", tabSize);
|
||||||
this.editor.on("blur", () => {
|
this.editor.on("blur", this._updateCode);
|
||||||
const editorValue = this.editor.getValue();
|
this.interval = setInterval(this._updateCode, 3000);
|
||||||
const propsValue = this.props[this.state.currentTab];
|
|
||||||
if (editorValue !== propsValue) {
|
|
||||||
this.trigger("updateCode", {
|
|
||||||
type: this.state.currentTab,
|
|
||||||
value: editorValue
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
willUnmount() {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
this.editor.off("blur", this._updateCode);
|
||||||
|
}
|
||||||
willUpdateProps(nextProps) {
|
willUpdateProps(nextProps) {
|
||||||
this._setupSessions(nextProps);
|
this._setupSessions(nextProps);
|
||||||
}
|
}
|
||||||
@@ -195,13 +237,15 @@ class TabbedEditor extends owl.Component {
|
|||||||
const session = this.sessions[this.state.currentTab];
|
const session = this.sessions[this.state.currentTab];
|
||||||
let content = this.props[this.state.currentTab];
|
let content = this.props[this.state.currentTab];
|
||||||
if (content === false) {
|
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];
|
content = this.props[tab];
|
||||||
this.state.currentTab = tab;
|
this.state.currentTab = tab;
|
||||||
}
|
}
|
||||||
session.setValue(content, -1);
|
if (this.editor.getValue() !== content) {
|
||||||
this.editor.setSession(session);
|
session.setValue(content, -1);
|
||||||
this.editor.resize();
|
this.editor.setSession(session);
|
||||||
|
this.editor.resize();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTab(tab) {
|
setTab(tab) {
|
||||||
@@ -230,7 +274,7 @@ class TabbedEditor extends owl.Component {
|
|||||||
|
|
||||||
_setupSessions(props) {
|
_setupSessions(props) {
|
||||||
for (let tab of ["js", "xml", "css"]) {
|
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] = new ace.EditSession(props[tab], MODES[tab]);
|
||||||
this.sessions[tab].setOption("useWorker", false);
|
this.sessions[tab].setOption("useWorker", false);
|
||||||
const tabSize = tab === "xml" ? 2 : 4;
|
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) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
this.version = owl.__info__.version;
|
this.version = owl.__info__.version;
|
||||||
this.SAMPLES = SAMPLES;
|
this.SAMPLES = useSamples();
|
||||||
|
this.isDirty = false;
|
||||||
|
|
||||||
this.state = useState({
|
this.state = useState({
|
||||||
js: SAMPLES[0].code,
|
js: this.SAMPLES[0].code,
|
||||||
css: SAMPLES[0].css || "",
|
css: this.SAMPLES[0].css || "",
|
||||||
xml: SAMPLES[0].xml || DEFAULT_XML,
|
xml: this.SAMPLES[0].xml || DEFAULT_XML,
|
||||||
error: false,
|
error: false,
|
||||||
displayWelcome: true,
|
displayWelcome: true,
|
||||||
splitLayout: true,
|
splitLayout: true,
|
||||||
@@ -302,10 +358,12 @@ class App extends owl.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSample(ev) {
|
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.js = sample.code;
|
||||||
this.state.css = sample.css || "";
|
this.state.css = sample.css || "";
|
||||||
this.state.xml = sample.xml || DEFAULT_XML;
|
this.state.xml = sample.xml || DEFAULT_XML;
|
||||||
|
deleteLocalSample();
|
||||||
|
this.isDirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get leftPaneStyle() {
|
get leftPaneStyle() {
|
||||||
@@ -338,7 +396,10 @@ class App extends owl.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
updateCode(ev) {
|
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() {
|
toggleLayout() {
|
||||||
this.state.splitLayout = !this.state.splitLayout;
|
this.state.splitLayout = !this.state.splitLayout;
|
||||||
@@ -363,7 +424,6 @@ class App extends owl.Component {
|
|||||||
}
|
}
|
||||||
App.components = { TabbedEditor };
|
App.components = { TabbedEditor };
|
||||||
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Application initialization
|
// Application initialization
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
+151
-226
@@ -318,6 +318,66 @@ const HOOKS_CSS = `button {
|
|||||||
font-size: 16px;
|
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
|
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
|
// www.todomvc.com project. This is a non trivial application with some
|
||||||
// interesting user interactions. It uses the local storage for persistence.
|
// 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
|
// In this implementation, we use the owl Store class to manage the state. It
|
||||||
// is very similar to the VueX store.
|
// is very similar to the VueX store.
|
||||||
const { Component, useState } = owl;
|
const { Component, useState } = owl;
|
||||||
const { useRef } = owl.hooks;
|
const { useRef, useStore, useDispatch, onPatched, onMounted } = owl.hooks;
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Constants, helpers
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
const ENTER_KEY = 13;
|
const ENTER_KEY = 13;
|
||||||
const ESC_KEY = 27;
|
const ESC_KEY = 27;
|
||||||
const LOCALSTORAGE_KEY = "todomvc";
|
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 = {
|
const actions = {
|
||||||
addTodo({ state }, title) {
|
addTodo({ state }, title) {
|
||||||
state.todos.push({
|
const todo = {
|
||||||
id: state.nextId++,
|
id: state.nextId++,
|
||||||
title,
|
title,
|
||||||
completed: false
|
completed: false
|
||||||
});
|
}
|
||||||
|
state.todos.push(todo);
|
||||||
},
|
},
|
||||||
removeTodo({ state }, id) {
|
removeTodo({ state }, id) {
|
||||||
const index = state.todos.findIndex(t => t.id === id);
|
const index = state.todos.findIndex(t => t.id === id);
|
||||||
state.todos.splice(index, 1);
|
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);
|
const todo = state.todos.find(t => t.id === id);
|
||||||
dispatch("editTodo", { id, completed: !todo.completed });
|
todo.completed = !todo.completed;
|
||||||
},
|
},
|
||||||
clearCompleted({ state, dispatch }) {
|
clearCompleted({ state, dispatch }) {
|
||||||
state.todos
|
for (let todo of state.todos) {
|
||||||
.filter(todo => todo.completed)
|
if (todo.completed) {
|
||||||
.forEach(todo => {
|
|
||||||
dispatch("removeTodo", todo.id);
|
dispatch("removeTodo", todo.id);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
toggleAll({ state, dispatch }, completed) {
|
toggleAll({ state, dispatch }, completed) {
|
||||||
state.todos.forEach(todo => {
|
for (let todo of state.todos) {
|
||||||
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) {
|
|
||||||
todo.completed = completed;
|
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
|
// TodoItem
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
class TodoItem extends Component {
|
class TodoItem extends Component {
|
||||||
state = useState({ isEditing: false });
|
state = useState({ isEditing: false });
|
||||||
inputRef = useRef("input");
|
dispatch = useDispatch();
|
||||||
|
|
||||||
removeTodo() {
|
constructor(...args) {
|
||||||
this.env.store.dispatch("removeTodo", this.props.id);
|
super(...args);
|
||||||
|
useAutofocus("input");
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTodo() {
|
editTodo() {
|
||||||
this.env.store.dispatch("toggleTodo", this.props.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async editTodo() {
|
|
||||||
this.state.isEditing = true;
|
this.state.isEditing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
focusInput() {
|
|
||||||
this.inputRef.el.value = "";
|
|
||||||
this.inputRef.el.focus();
|
|
||||||
this.inputRef.el.value = this.props.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyup(ev) {
|
handleKeyup(ev) {
|
||||||
if (ev.keyCode === ENTER_KEY) {
|
if (ev.keyCode === ENTER_KEY) {
|
||||||
this.updateTitle(ev.target.value);
|
this.updateTitle(ev.target.value);
|
||||||
@@ -433,48 +488,34 @@ class TodoItem extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateTitle(title) {
|
updateTitle(title) {
|
||||||
const value = title.trim();
|
this.dispatch("updateTodo", {title, id: this.props.id});
|
||||||
if (!value) {
|
this.state.isEditing = false;
|
||||||
this.removeTodo(this.props.id);
|
|
||||||
} else {
|
|
||||||
this.env.store.dispatch("editTodo", {
|
|
||||||
id: this.props.id,
|
|
||||||
title: value
|
|
||||||
});
|
|
||||||
this.state.isEditing = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// TodoApp
|
// TodoApp
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
class TodoApp extends owl.store.ConnectedComponent {
|
class TodoApp extends Component {
|
||||||
static components = { TodoItem };
|
static components = { TodoItem };
|
||||||
state = useState({ filter: "all" });
|
state = useState({ filter: "all" });
|
||||||
|
todos = useStore(state => state.todos);
|
||||||
|
dispatch = useDispatch();
|
||||||
|
|
||||||
static mapStoreToProps(state) {
|
|
||||||
return {
|
|
||||||
todos: state.todos
|
|
||||||
};
|
|
||||||
}
|
|
||||||
get visibleTodos() {
|
get visibleTodos() {
|
||||||
let todos = this.storeProps.todos;
|
switch (this.state.filter) {
|
||||||
if (this.state.filter === "active") {
|
case "active": return this.todos.filter(t => !t.completed);
|
||||||
todos = 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() {
|
get allChecked() {
|
||||||
return this.storeProps.todos.every(todo => todo.completed);
|
return this.todos.every(todo => todo.completed);
|
||||||
}
|
}
|
||||||
|
|
||||||
get remaining() {
|
get remaining() {
|
||||||
return this.storeProps.todos.filter(todo => !todo.completed).length;
|
return this.todos.filter(todo => !todo.completed).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
get remainingText() {
|
get remainingText() {
|
||||||
@@ -500,12 +541,25 @@ class TodoApp extends owl.store.ConnectedComponent {
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// App Initialization
|
// App Initialization
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
const store = makeStore();
|
function saveState(state) {
|
||||||
const qweb = new owl.QWeb(TEMPLATES);
|
const str = JSON.stringify(state);
|
||||||
const env = {
|
window.localStorage.setItem(LOCALSTORAGE_KEY, str);
|
||||||
qweb,
|
}
|
||||||
store,
|
|
||||||
};
|
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);
|
const app = new TodoApp(env);
|
||||||
app.mount(document.body);
|
app.mount(document.body);
|
||||||
`;
|
`;
|
||||||
@@ -516,7 +570,7 @@ const TODO_APP_STORE_XML = `<templates>
|
|||||||
<h1>todos</h1>
|
<h1>todos</h1>
|
||||||
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
|
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
|
||||||
</header>
|
</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)"/>
|
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="dispatch('toggleAll', !allChecked)"/>
|
||||||
<label for="toggle-all"></label>
|
<label for="toggle-all"></label>
|
||||||
<ul class="todo-list">
|
<ul class="todo-list">
|
||||||
@@ -525,7 +579,7 @@ const TODO_APP_STORE_XML = `<templates>
|
|||||||
</t>
|
</t>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<footer class="footer" t-if="storeProps.todos.length">
|
<footer class="footer" t-if="todos.length">
|
||||||
<span class="todo-count">
|
<span class="todo-count">
|
||||||
<strong>
|
<strong>
|
||||||
<t t-esc="remaining"/>
|
<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>
|
<a t-on-click="setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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
|
Clear completed
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</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}">
|
<li t-name="TodoItem" class="todo" t-att-class="{completed: props.completed, editing: state.isEditing}">
|
||||||
<div class="view">
|
<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">
|
<label t-on-dblclick="editTodo">
|
||||||
<t t-esc="props.title"/>
|
<t t-esc="props.title"/>
|
||||||
</label>
|
</label>
|
||||||
<button class="destroy" t-on-click="removeTodo"></button>
|
<button class="destroy" t-on-click="dispatch('removeTodo', props.id)"></button>
|
||||||
</div>
|
</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>
|
</li>
|
||||||
</templates>`;
|
</templates>`;
|
||||||
|
|
||||||
@@ -1625,135 +1679,6 @@ const WMS_CSS = `body {
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const MONKEYPATCH = `
|
|
||||||
// In this example, we show a possible way components can be monkey patched.
|
|
||||||
// This involves the functions patch/unpatch, which modifies the prototype of a
|
|
||||||
// class (not only an owl component). Sadly, the patch code cannot make use of
|
|
||||||
// the syntactic sugar that JS engines provides to classes, so it needs to use
|
|
||||||
// the "this._super()" syntax instead of the standard "super.someMethod()".
|
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
|
||||||
// Patching Code
|
|
||||||
//--------------------------------------------------------------------------
|
|
||||||
const patchMap = new WeakMap();
|
|
||||||
|
|
||||||
function patch(C, patchName, patch) {
|
|
||||||
let metadata = patchMap.get(C.prototype);
|
|
||||||
if (!metadata) {
|
|
||||||
metadata = {
|
|
||||||
origMethods: {},
|
|
||||||
patches: {},
|
|
||||||
current: []
|
|
||||||
};
|
|
||||||
patchMap.set(C.prototype, metadata);
|
|
||||||
}
|
|
||||||
const proto = C.prototype;
|
|
||||||
if (metadata.patches[patchName]) {
|
|
||||||
throw new Error(\`Patch [\${patchName}] already exists\`);
|
|
||||||
}
|
|
||||||
metadata.patches[patchName] = patch;
|
|
||||||
applyPatch(proto, patch);
|
|
||||||
metadata.current.push(patchName);
|
|
||||||
|
|
||||||
function applyPatch(proto, patch) {
|
|
||||||
Object.keys(patch).forEach(function(methodName) {
|
|
||||||
const method = patch[methodName];
|
|
||||||
if (typeof method === "function") {
|
|
||||||
const original = proto[methodName];
|
|
||||||
if (!(methodName in metadata.origMethods)) {
|
|
||||||
metadata.origMethods[methodName] = original;
|
|
||||||
}
|
|
||||||
proto[methodName] = function(...args) {
|
|
||||||
this._super = original;
|
|
||||||
return method.call(this, ...args);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we define here an unpatch function. This is mostly useful if we want to
|
|
||||||
// remove a patch. For example, for testing purposes
|
|
||||||
function unpatch(C, patchName) {
|
|
||||||
const proto = C.prototype;
|
|
||||||
let metadata = patchMap.get(proto);
|
|
||||||
if (!metdata) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
patchMap.delete(proto);
|
|
||||||
|
|
||||||
// reset to original
|
|
||||||
for (let k in metadata.origMethods) {
|
|
||||||
proto[k] = metadata.origMethods[k];
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply other patches
|
|
||||||
for (let name of metadata.current) {
|
|
||||||
if (name !== patchName) {
|
|
||||||
patch(C, name, metadata.patches[name]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
|
||||||
// Components
|
|
||||||
//--------------------------------------------------------------------------
|
|
||||||
const { Component, useState } = owl;
|
|
||||||
|
|
||||||
class Counter extends Component {
|
|
||||||
state = useState({ value: 0 });
|
|
||||||
|
|
||||||
increment() {
|
|
||||||
this.state.value++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main root component
|
|
||||||
class App extends Component {
|
|
||||||
static components = { Counter };
|
|
||||||
}
|
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
|
||||||
// Patching code
|
|
||||||
//--------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// In an Odoo application, this code would be located in another odoo module,
|
|
||||||
// to customize some behaviour in an existing class/component
|
|
||||||
patch(Counter, "double_value", {
|
|
||||||
increment() {
|
|
||||||
this._super();
|
|
||||||
this.state.value = 2 * this.state.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
|
||||||
// Application setup
|
|
||||||
//--------------------------------------------------------------------------
|
|
||||||
const qweb = new owl.QWeb(TEMPLATES);
|
|
||||||
const app = new App({ qweb });
|
|
||||||
app.mount(document.body);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MONKEYPATCH_XML = `
|
|
||||||
<templates>
|
|
||||||
<button t-name="Counter" t-on-click="increment">
|
|
||||||
Click [<t t-esc="state.value"/>]
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div t-name="App">
|
|
||||||
<Counter />
|
|
||||||
</div>
|
|
||||||
</templates>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MONKEYPATCH_CSS = `
|
|
||||||
button {
|
|
||||||
font-size: 20px;
|
|
||||||
width: 100px;
|
|
||||||
height: 50px;
|
|
||||||
}`;
|
|
||||||
|
|
||||||
export const SAMPLES = [
|
export const SAMPLES = [
|
||||||
{
|
{
|
||||||
description: "Components",
|
description: "Components",
|
||||||
@@ -1784,6 +1709,11 @@ export const SAMPLES = [
|
|||||||
xml: HOOKS_DEMO_XML,
|
xml: HOOKS_DEMO_XML,
|
||||||
css: HOOKS_CSS
|
css: HOOKS_CSS
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Context",
|
||||||
|
code: CONTEXT_JS,
|
||||||
|
xml: CONTEXT_XML,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "Todo List App (with store)",
|
description: "Todo List App (with store)",
|
||||||
code: TODO_APP_STORE,
|
code: TODO_APP_STORE,
|
||||||
@@ -1813,10 +1743,5 @@ export const SAMPLES = [
|
|||||||
code: ASYNC_COMPONENTS,
|
code: ASYNC_COMPONENTS,
|
||||||
xml: ASYNC_COMPONENTS_XML,
|
xml: ASYNC_COMPONENTS_XML,
|
||||||
css: ASYNC_COMPONENTS_CSS
|
css: ASYNC_COMPONENTS_CSS
|
||||||
}, {
|
|
||||||
description: "Monkey patching components",
|
|
||||||
code: MONKEYPATCH,
|
|
||||||
xml: MONKEYPATCH_XML,
|
|
||||||
css: MONKEYPATCH_CSS
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user