mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f289cb17ec | |||
| e6a3ede4a6 | |||
| 10cfe0b740 | |||
| a3d563ebb4 | |||
| 3b1247d476 | |||
| beb6c2bc82 | |||
| f0cd6f61cd | |||
| c950b385a0 | |||
| c172b31481 | |||
| 9b3a97e732 | |||
| 670c98916c | |||
| c59abef374 | |||
| bb4c2506c0 | |||
| 9843d16585 | |||
| 34e463ad0a | |||
| 2294c1967f | |||
| 4b53e48e91 |
@@ -4,16 +4,20 @@ _A web framework for structured, dynamic and maintainable applications_
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
The Odoo Web Library (OWL) is a small
|
The Odoo Web Library (OWL) is a small (~16kb gzipped) UI framework intended to be the basis for
|
||||||
UI framework intended to be the basis for the [Odoo](https://www.odoo.com/) Web Client, and hopefully many
|
the [Odoo](https://www.odoo.com/) Web Client, and hopefully many other Odoo
|
||||||
other Odoo related projects. OWL's main feature is a _declarative component system_, with QWeb as a template engine, asynchronous rendering, and an underlying virtual dom.
|
related projects. OWL's main features are:
|
||||||
|
|
||||||
|
- a _declarative component system_, (template based, with asynchronous rendering and a virtual dom)
|
||||||
|
- a store implementation (for state management)
|
||||||
|
- a small frontend router
|
||||||
|
|
||||||
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the OWL framework.
|
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the OWL framework.
|
||||||
|
|
||||||
## OWL's Design Principles
|
## OWL's Design Principles
|
||||||
|
|
||||||
OWL is designed to be used in highly dynamic applications where changing
|
OWL is designed to be used in highly dynamic applications where changing
|
||||||
requirements are common, code needs to be maintained by large teams.
|
requirements are common and code needs to be maintained by large teams.
|
||||||
|
|
||||||
- **XML based**: templates are based on the XML format, which allows interesting
|
- **XML based**: templates are based on the XML format, which allows interesting
|
||||||
applications. For example, they could be stored in a database and modified
|
applications. For example, they could be stored in a database and modified
|
||||||
@@ -26,8 +30,11 @@ requirements are common, code needs to be maintained by large teams.
|
|||||||
for various reasons (security/deployment/dynamic modules/specific assets tools),
|
for various reasons (security/deployment/dynamic modules/specific assets tools),
|
||||||
it is not ok to use standard web tools based on `npm`.
|
it is not ok to use standard web tools based on `npm`.
|
||||||
|
|
||||||
Owl is not designed to be fast or small (even though it is quite good on those
|
Owl is not designed to be fast nor small (even though it is quite good on those
|
||||||
two topics). If you are interested in a comparison with React or Vue, you will
|
two topics). It is a no nonsense framework to build applications. There is only
|
||||||
|
one way to define components (with classes).
|
||||||
|
|
||||||
|
If you are interested in a comparison with React or Vue, you will
|
||||||
find some more information [here](doc/comparison.md).
|
find some more information [here](doc/comparison.md).
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
@@ -64,8 +71,8 @@ string. More interesting examples can be found on the
|
|||||||
|
|
||||||
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.18.0.js](https://github.com/odoo/owl/releases/download/v0.18.0/owl.js)
|
- [owl-0.19.0.js](https://github.com/odoo/owl/releases/download/v0.19.0/owl.js)
|
||||||
- [owl-0.18.0.min.js](https://github.com/odoo/owl/releases/download/v0.18.0/owl.min.js)
|
- [owl-0.19.0.min.js](https://github.com/odoo/owl/releases/download/v0.19.0/owl.min.js)
|
||||||
|
|
||||||
Some npm scripts are available:
|
Some npm scripts are available:
|
||||||
|
|
||||||
|
|||||||
+53
-13
@@ -10,6 +10,7 @@ discussed, feel free to open an issue/submit a PR to correct this text.
|
|||||||
## Content
|
## Content
|
||||||
|
|
||||||
- [Size](#size)
|
- [Size](#size)
|
||||||
|
- [Class Based](#class-based)
|
||||||
- [Tooling/Build Step](#toolingbuild-step)
|
- [Tooling/Build Step](#toolingbuild-step)
|
||||||
- [Templating](#templating)
|
- [Templating](#templating)
|
||||||
- [Asynchronous rendering](#asynchronous-rendering)
|
- [Asynchronous rendering](#asynchronous-rendering)
|
||||||
@@ -21,12 +22,23 @@ discussed, feel free to open an issue/submit a PR to correct this text.
|
|||||||
OWL is intended to be small and to work at a slightly lower level of abstraction
|
OWL is intended to be small and to work at a slightly lower level of abstraction
|
||||||
than React and Vue. Also, jQuery is not the same kind of framework, but it is interesting to compare.
|
than React and Vue. Also, jQuery is not the same kind of framework, but it is interesting to compare.
|
||||||
|
|
||||||
| Framework | Size (minified) | Size (minified, gzipped) |
|
| Framework | Size (minified, gzipped) |
|
||||||
| ------------------------ | --------------- | ------------------------ |
|
| ------------------------ | ------------------------ |
|
||||||
| OWL | 32kb | 11kb |
|
| OWL | 16kb |
|
||||||
| Vue + VueX | | 30kb |
|
| Vue + VueX | 30kb |
|
||||||
| React + ReactDOM + Redux | | 40kb |
|
| React + ReactDOM + Redux | 40kb |
|
||||||
| jQuery | 86kb | 30kb |
|
| jQuery | 30kb |
|
||||||
|
|
||||||
|
## Class Based
|
||||||
|
|
||||||
|
Both React and Vue moved away from defining components with classes. They prefer
|
||||||
|
a more functional approach, in particular, with the new `hooks` mechanisms.
|
||||||
|
|
||||||
|
This has some advantages and disadvantages. But the end result is that React
|
||||||
|
and Vue both offers multiple different ways of defining new components. In
|
||||||
|
contrast, Owl has only one mechanism: class-based components. We believe that Owl
|
||||||
|
components are fast enough for all our usecases, and making it as simple as
|
||||||
|
possible for developers is more valuable (for us).
|
||||||
|
|
||||||
## Tooling/Build step
|
## Tooling/Build step
|
||||||
|
|
||||||
@@ -45,7 +57,7 @@ components, which also necessitate a build step.
|
|||||||
On the flipside, external tooling may make it harder to use in some case, but it
|
On the flipside, external tooling may make it harder to use in some case, but it
|
||||||
also brings a lot of benefits. And React/Vue have both a large ecosystem.
|
also brings a lot of benefits. And React/Vue have both a large ecosystem.
|
||||||
|
|
||||||
### Templating
|
## Templating
|
||||||
|
|
||||||
OWL uses its own QWeb engine, which compiles templates on the
|
OWL uses its own QWeb engine, which compiles templates on the
|
||||||
frontend, as they are needed. This is extremely convenient for our use case, in
|
frontend, as they are needed. This is extremely convenient for our use case, in
|
||||||
@@ -128,8 +140,10 @@ by getters/setters. With that, it can notify components whenever the state that
|
|||||||
they read was changed.
|
they read was changed.
|
||||||
|
|
||||||
Owl is closer to vue: it also tracks magically the state properties, but it does
|
Owl is closer to vue: it also tracks magically the state properties, but it does
|
||||||
only increment a counter whenever it changes (and a _deep_ counter for each of
|
only increment an internal counter whenever it changes. Note that it is done
|
||||||
its parents). This assumes that the state is actually a tree.
|
with a `Proxy`, which means that it is totally transparent to the developers.
|
||||||
|
Adding new keys is supported. Once any part of the state has been changed, a
|
||||||
|
rendering is scheduled in the next microtask tick (promise queue).
|
||||||
|
|
||||||
## State Management
|
## State Management
|
||||||
|
|
||||||
@@ -190,7 +204,33 @@ keeps track of who get data, and retrigger a render when it was changed.
|
|||||||
|
|
||||||
**Owl**
|
**Owl**
|
||||||
|
|
||||||
Owl store is a little bit like a mix of redux and vuex: it has mutations and
|
Owl store is a little bit like a mix of redux and vuex: it has actions (but not
|
||||||
actions, like VueX, it keeps track of the state changes, but it does not notify
|
mutations), and like VueX, it keeps track of the state changes. However, it does
|
||||||
a component when the state changes. Instead, components need to connect to the
|
not notify a component when the state changes. Instead, components need to connect
|
||||||
store like in redux, with a function that will listen to the relevant state.
|
to the store like in redux, by inheriting the `ConnectedComponent` class.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const actions = {
|
||||||
|
increment({ state }, val) {
|
||||||
|
state.counter += val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
counter: 0
|
||||||
|
};
|
||||||
|
const store = new owl.Store({ state, actions });
|
||||||
|
|
||||||
|
class Counter extends owl.ConnectedComponent {
|
||||||
|
static mapStoreToProps(state) {
|
||||||
|
return {
|
||||||
|
value: state.counter
|
||||||
|
};
|
||||||
|
}
|
||||||
|
increment() {
|
||||||
|
this.env.store.dispatch("increment");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const counter = new Counter({ store, qweb });
|
||||||
|
```
|
||||||
+9
-5
@@ -144,12 +144,16 @@ find a template with the component name (or one of its ancestor).
|
|||||||
|
|
||||||
We explain here all the public methods of the `Component` class.
|
We explain here all the public methods of the `Component` class.
|
||||||
|
|
||||||
- **`mount(target)`** (async): this is the main way a component's hierarchy is added to the
|
- **`mount(target, renderBeforeRemount=false)`** (async): this is the main way a
|
||||||
DOM: the root component is mounted to a target HTMLElement. Obviously, this
|
component is added to the DOM: the root component is mounted to a target
|
||||||
is asynchronous, since each children need to be created as well. Most applications
|
HTMLElement. Obviously, this is asynchronous, since each children need to be
|
||||||
will need to call `mount` exactly once, on the root component.
|
created as well. Most applications will need to call `mount` exactly once, on
|
||||||
|
the root component.
|
||||||
|
|
||||||
Note that a component can be mounted and unmounted multiple times if needed.
|
The `renderBeforeRemount` argument is useful when a component is unmounted and remounted.
|
||||||
|
In that case, we may want to rerender the component _before_ it is remounted, if
|
||||||
|
we know that its state (or something in the environment, or ...) has changed.
|
||||||
|
In that case, it should simply set to `true`.
|
||||||
|
|
||||||
- **`unmount()`**: in case a component need to be detached/removed from the DOM, this
|
- **`unmount()`**: in case a component need to be detached/removed from the DOM, this
|
||||||
method can be used. Most applications should not call `unmount`, this is more
|
method can be used. Most applications should not call `unmount`, this is more
|
||||||
|
|||||||
+8
-3
@@ -12,15 +12,18 @@ owl
|
|||||||
core
|
core
|
||||||
EventBus
|
EventBus
|
||||||
Observer
|
Observer
|
||||||
|
router
|
||||||
|
Link
|
||||||
|
Router
|
||||||
store
|
store
|
||||||
Store
|
Store
|
||||||
ConnectedComponent
|
ConnectedComponent
|
||||||
utils
|
utils
|
||||||
whenReady
|
debounce
|
||||||
|
escape
|
||||||
loadJS
|
loadJS
|
||||||
loadTemplates
|
loadTemplates
|
||||||
escape
|
whenReady
|
||||||
debounce
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
@@ -30,6 +33,7 @@ owl
|
|||||||
- [Event Bus](event_bus.md)
|
- [Event Bus](event_bus.md)
|
||||||
- [Observer](observer.md)
|
- [Observer](observer.md)
|
||||||
- [QWeb](qweb.md)
|
- [QWeb](qweb.md)
|
||||||
|
- [Router](router.md)
|
||||||
- [Store](store.md)
|
- [Store](store.md)
|
||||||
- [Utils](utils.md)
|
- [Utils](utils.md)
|
||||||
- [Virtual DOM](vdom.md)
|
- [Virtual DOM](vdom.md)
|
||||||
@@ -42,3 +46,4 @@ owl
|
|||||||
|
|
||||||
- [Comparison with React/Vue](comparison.md)
|
- [Comparison with React/Vue](comparison.md)
|
||||||
- [Tooling](tooling.md)
|
- [Tooling](tooling.md)
|
||||||
|
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
|
||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
# 🦉 Router 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Example](#example)
|
||||||
|
- [Reference](#reference)
|
||||||
|
- [Route Definition](#route-definition)
|
||||||
|
- [Router](#router)
|
||||||
|
- [Navigation Guards](#navigation-guards)
|
||||||
|
- [`t-routecomponent`](#t-routecomponent)
|
||||||
|
- [Link](#link)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
It is often useful to organize an application around urls. If the application is
|
||||||
|
a single page application, then we need a way to manage those urls in the browser.
|
||||||
|
This is why there are many different routers for different frameworks. A generic
|
||||||
|
router can do the job just fine, but a specialized router for Owl can give a
|
||||||
|
better developer experience.
|
||||||
|
|
||||||
|
The Owl router support the following features:
|
||||||
|
|
||||||
|
- `history` or `hash` mode
|
||||||
|
- declarative routes
|
||||||
|
- route redirection
|
||||||
|
- navigation guards
|
||||||
|
- parameterized routes
|
||||||
|
- a `<Link>` component
|
||||||
|
- a `t-routecomponent` directive
|
||||||
|
|
||||||
|
Note that it is still in early stage of developments, and there are probably
|
||||||
|
still some issues.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
To use the Owl router, there are some steps that needs to be done:
|
||||||
|
|
||||||
|
- declare some routes
|
||||||
|
- create a router
|
||||||
|
- add it to the environment
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function protectRoute({ env, to }) {
|
||||||
|
if (!env.session.authUser) {
|
||||||
|
env.session.setNextRoute(to.name);
|
||||||
|
return { to: "SIGN_IN" };
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ROUTES = [
|
||||||
|
{ name: "LANDING", path: "/", component: Landing },
|
||||||
|
{ name: "TASK", path: "/tasks/{{id}}", component: Task },
|
||||||
|
{ name: "SIGN_UP", path: "/signup", component: SignUp },
|
||||||
|
{ name: "SIGN_IN", path: "/signin", component: SignIn },
|
||||||
|
{ name: "ADMIN", path: "/admin", component: Admin, beforeRouteEnter: protectRoute },
|
||||||
|
{ name: "ACCOUNT", path: "/account", component: Account, beforeRouteEnter: protectRoute },
|
||||||
|
{ name: "UNKNOWN", path: "*", redirect: { to: "LANDING" } }
|
||||||
|
];
|
||||||
|
|
||||||
|
function makeEnvironment() {
|
||||||
|
...
|
||||||
|
const env = { qweb };
|
||||||
|
env.session = new Session(env);
|
||||||
|
env.router = new owl.router.Router(env, ROUTES);
|
||||||
|
await env.router.start();
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that the router needs to be started. This is an asynchronous operation
|
||||||
|
because it needs to apply the potential navigation guards on the current route
|
||||||
|
(which may or may not mean that the application is redirected to another route).
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
### Route definition
|
||||||
|
|
||||||
|
A route need to be defined as an object with the following keys:
|
||||||
|
|
||||||
|
- `name` (optional): a (unique) string useful to identify the current route. If not
|
||||||
|
given, it will be assigned an automatic name,
|
||||||
|
- `path`: a string describing the url. It can be static: `/admin` or dynamic: `/users/{{id}}`.
|
||||||
|
It also can be `*`, to catch all remaining routes.
|
||||||
|
- `component` (optional): an Owl component that will be used by the `t-routecomponent`
|
||||||
|
directive if the route is active
|
||||||
|
- `redirect` (optional): should be destination object (with optional keys `path`, `to` and `params`) if given, the application will be redirected to the destination whenever we match this route
|
||||||
|
- `beforeRouteEnter`: defines a [navigation guard](#navigation-guards).
|
||||||
|
|
||||||
|
### `Router`
|
||||||
|
|
||||||
|
The `Router` constructor takes three arguments:
|
||||||
|
|
||||||
|
- `env`: a valid environment,
|
||||||
|
- a list of routes,
|
||||||
|
- an optional object (with the only key `mode` which can be `history` (default
|
||||||
|
value) or `hash`).
|
||||||
|
|
||||||
|
```js
|
||||||
|
const ROUTES = [...];
|
||||||
|
const router = new owl.router.Router(env, ROUTES, {mode: 'history'});
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the route are defined in a list, and the order matters: the router
|
||||||
|
tries to find a match by going down the list.
|
||||||
|
|
||||||
|
The router needs to be added to the environment in the `router` sub key.
|
||||||
|
|
||||||
|
Once a router is created, it needs to be started. This is necessary to initialize
|
||||||
|
its current state to the current URL (and also, to potentially apply any
|
||||||
|
navigation guards and/or redirecting).
|
||||||
|
|
||||||
|
```js
|
||||||
|
await router.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
Once started, the router will keep track of the current url and reflect its
|
||||||
|
value in two keys:
|
||||||
|
|
||||||
|
- `router.currentRoute`
|
||||||
|
- `router.currentParams`
|
||||||
|
|
||||||
|
The router also has a `navigate` method, useful to programmatically change the
|
||||||
|
application to another state (and the url):
|
||||||
|
|
||||||
|
```js
|
||||||
|
router.navigate({to: 'USER', params: {id: 51}});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Navigation Guards
|
||||||
|
|
||||||
|
Navigation guards are very useful to be able to execute some business logic/
|
||||||
|
perform some actions or redirect to other routes whenever the application is
|
||||||
|
entering a new route. For example, the following guard checks if there is an
|
||||||
|
authenticated user, and if it is not the case, redirect to the sign in route.
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function protectRoute({ env, to }) {
|
||||||
|
if (!env.session.authUser) {
|
||||||
|
env.session.setNextRoute(to.name);
|
||||||
|
return { to: "SIGN_IN" };
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A navigation guard is a function that returns a promise, which either resolves
|
||||||
|
to `true` (the navigation is accepted), or to another destination object.
|
||||||
|
|
||||||
|
### `t-routecomponent`
|
||||||
|
|
||||||
|
The `t-routecomponent` directive directs Owl to render the component associated
|
||||||
|
to the currently active route (if any):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="App">
|
||||||
|
<NavBar />
|
||||||
|
<t t-routecomponent="1"/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### `Link`
|
||||||
|
|
||||||
|
The `Link` component is a Owl component which render as a `<a>` tag with any
|
||||||
|
content. It will compute the proper href from its props, and allow Owl to
|
||||||
|
properly navigate to a given url if clicked on it.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Link to="'HOME'">Home</Link>
|
||||||
|
```
|
||||||
+88
-53
@@ -5,8 +5,7 @@
|
|||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [Example](#example)
|
- [Example](#example)
|
||||||
- [Reference](#reference)
|
- [Reference](#reference)
|
||||||
- [Public API](#public-api)
|
- [Store](#store)
|
||||||
- [Mutations](#mutations)
|
|
||||||
- [Actions](#actions)
|
- [Actions](#actions)
|
||||||
- [Getters](#getters)
|
- [Getters](#getters)
|
||||||
- [Connecting a Component](#connecting-a-component)
|
- [Connecting a Component](#connecting-a-component)
|
||||||
@@ -22,11 +21,10 @@ 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 (through
|
some state, and let the developer update it in a structured way, with `actions`.
|
||||||
mutations and actions). Owl components can then connect to the store, and will
|
Owl components can then connect to the store, and will be updated if necessary.
|
||||||
be updated if necessary.
|
|
||||||
|
|
||||||
Note: Owl's store is inspired by React Redux and VueX.
|
Note: Owl store is inspired by React Redux and VueX.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
@@ -34,19 +32,12 @@ Here is what a simple store looks like:
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
const actions = {
|
const actions = {
|
||||||
addTodo({ commit }, message) {
|
|
||||||
commit("addTodo", message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mutations = {
|
|
||||||
addTodo({ state }, message) {
|
addTodo({ state }, message) {
|
||||||
const todo = {
|
state.todos.push({
|
||||||
id: state.nextId++,
|
id: state.nextId++,
|
||||||
message,
|
message,
|
||||||
isCompleted: false
|
isCompleted: false
|
||||||
};
|
});
|
||||||
state.todos.push(todo);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -55,66 +46,65 @@ const state = {
|
|||||||
nextId: 1
|
nextId: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
const store = new owl.Store({ state, actions, mutations });
|
const store = new owl.Store({ state, actions });
|
||||||
store.on("update", () => console.log(store.state));
|
store.on("update", () => console.log(store.state));
|
||||||
|
|
||||||
// updating the state
|
// updating the state
|
||||||
store.dispatch("addTodo", "fix all bugs");
|
store.dispatch("addTodo", "fix all bugs");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This example shows how a store can be defined and used. Note that in most cases,
|
||||||
|
actions will be dispatched by connected components.
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events whenever its
|
### `Store`
|
||||||
state is changed. Note that these events are triggered only after a microtask
|
|
||||||
tick, so only one event will be triggered for any number of state changes in a
|
The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events
|
||||||
|
whenever its state is changed. Note that these events are triggered only after a
|
||||||
|
microtask tick, so only one event will be triggered for any number of state changes in a
|
||||||
call stack.
|
call stack.
|
||||||
|
|
||||||
Also, it is important to mention that the state is observed (with an `owl.Observer`),
|
Also, it is important to mention that the state is observed (with an `owl.Observer`),
|
||||||
which is the reason why it is able to know if it was changed. This implies that
|
which is the reason why it is able to know if it was changed. See the
|
||||||
state changes need to be done carefully in some cases (adding a new key to an
|
|
||||||
object, or modifying an array with the `arr[i] = newValue` syntax). See the
|
|
||||||
[Observer](observer.md)'s documentation for more details.
|
[Observer](observer.md)'s documentation for more details.
|
||||||
|
|
||||||
### Public API
|
The `Store` class is quite small. It has two public methods:
|
||||||
|
|
||||||
1. `constructor`
|
- its constructor
|
||||||
2. `commit`
|
- `dispatch`
|
||||||
3. `dispatch`
|
|
||||||
|
|
||||||
### Mutations
|
The constructor takes a configuration object with four (optional) keys:
|
||||||
|
|
||||||
Mutations are the only way to modify the state. Changing the state outside a
|
- the initial state
|
||||||
mutation is not allowed (and should throw an error). Mutations are synchronous.
|
- the actions
|
||||||
|
- the getters
|
||||||
|
- the environment
|
||||||
|
|
||||||
```js
|
```javascript
|
||||||
const mutations = {
|
const config = {
|
||||||
setLoginState({ state }, loginState) {
|
state,
|
||||||
state.loginState = loginState;
|
actions,
|
||||||
}
|
getters,
|
||||||
|
env
|
||||||
};
|
};
|
||||||
```
|
const store = new Store(config);
|
||||||
|
|
||||||
Mutations are called with the `commit` method on the store, and can receive an arbitrary number of arguments.
|
|
||||||
|
|
||||||
```js
|
|
||||||
store.commit("setLoginState", "error");
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Actions
|
### Actions
|
||||||
|
|
||||||
Actions are used to coordinate state changes. It is also useful whenever some
|
Actions are used to coordinate state changes. It can be used for both synchronous
|
||||||
asynchronous logic is necessary. For example, fetching data should be done
|
and asynchronous logic.
|
||||||
in an action.
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const actions = {
|
const actions = {
|
||||||
async login({ commit }, info) {
|
async login({ state }, info) {
|
||||||
commit("setLoginState", "pending");
|
state.loginState = "pending";
|
||||||
try {
|
try {
|
||||||
const loginInfo = await doSomeRPC("/login/", info);
|
const loginInfo = await doSomeRPC("/login/", info);
|
||||||
commit("setLoginState", loginInfo);
|
state.loginState = loginInfo;
|
||||||
} catch {
|
} catch {
|
||||||
commit("setLoginState", "error");
|
state.loginState = "error";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -130,6 +120,36 @@ store.dispatch("login", someInfo);
|
|||||||
Note that anything returned by an action will also be returned by the `dispatch`
|
Note that anything returned by an action will also be returned by the `dispatch`
|
||||||
call.
|
call.
|
||||||
|
|
||||||
|
Also, it is important to be aware that we need to be careful with asynchronous
|
||||||
|
logic. Each state change will potentially trigger a rerendering, so we need to
|
||||||
|
make sure that we do not have a partial corrupted state. Here is an example that
|
||||||
|
is likely not a good idea:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const actions = {
|
||||||
|
async fetchSomeData({ state }, recordId) {
|
||||||
|
state.recordId = recordId;
|
||||||
|
const data = await doSomeRPC("/read/", recordId);
|
||||||
|
state.recordData = data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
In the previous example, there is a period of time in which the state has a
|
||||||
|
`recordId` which does not correspond to the `recordData`. It is more likely that
|
||||||
|
we want an atomic update: updating the `recordId` at the same time as the `recordData`
|
||||||
|
values:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const actions = {
|
||||||
|
async fetchSomeData({ state }, recordId) {
|
||||||
|
const data = await doSomeRPC("/read/", recordId);
|
||||||
|
state.recordId = recordId;
|
||||||
|
state.recordData = data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
### Getters
|
### Getters
|
||||||
|
|
||||||
Usually, data contained in the store will be stored in a normalized way. For
|
Usually, data contained in the store will be stored in a normalized way. For
|
||||||
@@ -181,19 +201,15 @@ need to create a component inheriting from `OwlComponent`:
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const actions = {
|
const actions = {
|
||||||
increment({ commit }) {
|
|
||||||
commit("increment", 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const mutations = {
|
|
||||||
increment({ state }, val) {
|
increment({ state }, val) {
|
||||||
state.counter += val;
|
state.counter += val;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
counter: 0
|
counter: 0
|
||||||
};
|
};
|
||||||
const store = new owl.Store({ state, actions, mutations });
|
const store = new owl.Store({ state, actions });
|
||||||
|
|
||||||
class Counter extends owl.ConnectedComponent {
|
class Counter extends owl.ConnectedComponent {
|
||||||
static mapStoreToProps(state) {
|
static mapStoreToProps(state) {
|
||||||
@@ -227,6 +243,25 @@ The `ConnectedComponent` class can be configured with the following fields:
|
|||||||
- `deep` (boolean): [only useful if no hashFunction is given] if `false`, only watch
|
- `deep` (boolean): [only useful if no hashFunction is given] if `false`, only watch
|
||||||
for top level state changes (`true` by default)
|
for top level state changes (`true` by default)
|
||||||
|
|
||||||
|
Note that the class `ConnectedComponent` has a `dispatch` method. This means
|
||||||
|
that the previous example could be simplified like this:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class Counter extends owl.ConnectedComponent {
|
||||||
|
static mapStoreToProps(state) {
|
||||||
|
return {
|
||||||
|
value: state.counter
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<button t-name="Counter" t-on-click="dispatch('increment')">
|
||||||
|
Click Me! [<t t-esc="props.value"/>]
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
### Semantics
|
### Semantics
|
||||||
|
|
||||||
The `Store` and the `ConnectedComponent` try to be smart and to optimize as much
|
The `Store` and the `ConnectedComponent` try to be smart and to optimize as much
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "owl",
|
"name": "owl",
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+88
-31
@@ -38,15 +38,21 @@ export interface Env {
|
|||||||
* useful to typecheck and describe the internal keys used by Owl to manage the
|
* useful to typecheck and describe the internal keys used by Owl to manage the
|
||||||
* component tree.
|
* component tree.
|
||||||
*/
|
*/
|
||||||
export interface Meta<T extends Env, Props> {
|
interface Internal<T extends Env, Props> {
|
||||||
|
// each component has a unique id, useful mostly to handle parent/child
|
||||||
|
// relationships
|
||||||
readonly id: number;
|
readonly id: number;
|
||||||
vnode: VNode | null;
|
vnode: VNode | null;
|
||||||
isMounted: boolean;
|
isMounted: boolean;
|
||||||
isDestroyed: boolean;
|
isDestroyed: boolean;
|
||||||
|
|
||||||
|
// parent and children keys are obviously useful to setup the parent-children
|
||||||
|
// relationship.
|
||||||
parent: Component<T, any, any> | null;
|
parent: Component<T, any, any> | null;
|
||||||
children: { [key: number]: Component<T, any, any> };
|
children: { [key: number]: Component<T, any, any> };
|
||||||
// children mapping: from templateID to componentID
|
// children mapping: from templateID to componentID. templateID identifies a
|
||||||
// should it be a map number => Component?
|
// place in a template. The t-component directive needs it to be able to get
|
||||||
|
// the component instance back whenever the template is rerendered.
|
||||||
cmap: { [key: number]: number };
|
cmap: { [key: number]: number };
|
||||||
|
|
||||||
renderId: number;
|
renderId: number;
|
||||||
@@ -58,10 +64,10 @@ export interface Meta<T extends Env, Props> {
|
|||||||
renderPromise: Promise<VNode> | null;
|
renderPromise: Promise<VNode> | null;
|
||||||
|
|
||||||
boundHandlers: { [key: number]: any };
|
boundHandlers: { [key: number]: any };
|
||||||
observer?: Observer;
|
observer: Observer | null;
|
||||||
render?: CompiledTemplate;
|
render: CompiledTemplate | null;
|
||||||
mountedHandlers: { [key: number]: Function };
|
mountedHandlers: { [key: number]: Function };
|
||||||
classObj?: { [key: string]: boolean };
|
classObj: { [key: string]: boolean } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a component does not define explicitely a template
|
// If a component does not define explicitely a template
|
||||||
@@ -76,7 +82,7 @@ const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
|
|||||||
let nextId = 1;
|
let nextId = 1;
|
||||||
|
|
||||||
export class Component<T extends Env, Props extends {}, State extends {}> {
|
export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||||
readonly __owl__: Meta<Env, Props>;
|
readonly __owl__: Internal<Env, Props>;
|
||||||
template?: string;
|
template?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -171,7 +177,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
renderPromise: null,
|
renderPromise: null,
|
||||||
renderProps: props || null,
|
renderProps: props || null,
|
||||||
boundHandlers: {},
|
boundHandlers: {},
|
||||||
mountedHandlers: {}
|
mountedHandlers: {},
|
||||||
|
observer: null,
|
||||||
|
render: null,
|
||||||
|
classObj: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,19 +278,22 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
*
|
*
|
||||||
* Note that a component can be mounted an unmounted several times
|
* Note that a component can be mounted an unmounted several times
|
||||||
*/
|
*/
|
||||||
async mount(target: HTMLElement): Promise<void> {
|
async mount(target: HTMLElement, renderBeforeRemount: boolean = false): Promise<void> {
|
||||||
if (this.__owl__.isMounted) {
|
const __owl__ = this.__owl__;
|
||||||
|
if (__owl__.isMounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.__owl__.vnode ) {
|
if (!__owl__.vnode) {
|
||||||
// we use the fact that renderId === 1 as a way to determine that the
|
|
||||||
// component is mounted for the first time
|
|
||||||
const vnode = await this.__prepare();
|
const vnode = await this.__prepare();
|
||||||
if (this.__owl__.isDestroyed) {
|
if (__owl__.isDestroyed) {
|
||||||
// component was destroyed before we get here...
|
// component was destroyed before we get here...
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.__patch(vnode);
|
this.__patch(vnode);
|
||||||
|
} else if (renderBeforeRemount) {
|
||||||
|
const patchQueue = [];
|
||||||
|
await this.__render(false, patchQueue, undefined, undefined);
|
||||||
|
this.__applyPatchQueue(<any[]>patchQueue);
|
||||||
}
|
}
|
||||||
target.appendChild(this.el!);
|
target.appendChild(this.el!);
|
||||||
|
|
||||||
@@ -290,6 +302,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unmount method is the opposite of the mount method. It is useful
|
||||||
|
* to call willUnmount calls and remove the component from the DOM.
|
||||||
|
*/
|
||||||
unmount() {
|
unmount() {
|
||||||
if (this.__owl__.isMounted) {
|
if (this.__owl__.isMounted) {
|
||||||
this.__callWillUnmount();
|
this.__callWillUnmount();
|
||||||
@@ -297,20 +313,26 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(force: boolean = false, patchQueue?: any[], scope?: any, vars?: any): Promise<void> {
|
/**
|
||||||
|
* The render method is the main entry point to render a component (once it
|
||||||
|
* is ready. This method is not initially called when the component is
|
||||||
|
* rendered the first time).
|
||||||
|
*
|
||||||
|
* This method will cause all its sub components to potentially rerender
|
||||||
|
* themselves. Note that `render` is not called if a component is updated via
|
||||||
|
* its props.
|
||||||
|
*/
|
||||||
|
async render(force: boolean = false): Promise<void> {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
if (!__owl__.isMounted) {
|
if (!__owl__.isMounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const shouldPatch: boolean = !patchQueue;
|
const patchQueue = [];
|
||||||
if (shouldPatch) {
|
|
||||||
patchQueue = [];
|
|
||||||
}
|
|
||||||
__owl__.renderId++
|
|
||||||
const renderId = __owl__.renderId;
|
|
||||||
await this.__render(force, patchQueue, scope, vars);
|
|
||||||
|
|
||||||
if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
|
const renderId = ++__owl__.renderId;
|
||||||
|
await this.__render(force, patchQueue, undefined, undefined);
|
||||||
|
|
||||||
|
if (__owl__.isMounted && renderId === __owl__.renderId) {
|
||||||
// we only update the vnode and the actual DOM if no other rendering
|
// we only update the vnode and the actual DOM if no other rendering
|
||||||
// occurred between now and when the render method was initially called.
|
// occurred between now and when the render method was initially called.
|
||||||
this.__applyPatchQueue(<any[]>patchQueue);
|
this.__applyPatchQueue(<any[]>patchQueue);
|
||||||
@@ -387,6 +409,17 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
// Private
|
// Private
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private helper to perform a full destroy, from the point of view of an Owl
|
||||||
|
* component. It does not remove the el (this is done only once on the top
|
||||||
|
* level destroyed component, for performance reasons).
|
||||||
|
*
|
||||||
|
* The job of this method is mostly to call willUnmount hooks, and to perform
|
||||||
|
* all necessary internal cleanup.
|
||||||
|
*
|
||||||
|
* Note that it does not call the __callWillUnmount method to avoid visiting
|
||||||
|
* all children many times.
|
||||||
|
*/
|
||||||
__destroy(parent: Component<any, any, any> | null) {
|
__destroy(parent: Component<any, any, any> | null) {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
const isMounted = __owl__.isMounted;
|
const isMounted = __owl__.isMounted;
|
||||||
@@ -441,6 +474,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The __updateProps method is called by the t-component directive whenever
|
||||||
|
* it updates a component (so, when the parent template is rerendered).
|
||||||
|
*/
|
||||||
async __updateProps(
|
async __updateProps(
|
||||||
nextProps: Props,
|
nextProps: Props,
|
||||||
forceUpdate: boolean = false,
|
forceUpdate: boolean = false,
|
||||||
@@ -456,16 +493,17 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
await this.willUpdateProps(nextProps);
|
await this.willUpdateProps(nextProps);
|
||||||
this.props = nextProps;
|
this.props = nextProps;
|
||||||
await this.render(forceUpdate, patchQueue, scope, vars);
|
await this.__render(forceUpdate, patchQueue, scope, vars);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main patching method. We call the virtual dom patch method here to convert
|
||||||
|
* a virtual dom vnode into some actual dom.
|
||||||
|
*/
|
||||||
__patch(vnode) {
|
__patch(vnode) {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
const target = __owl__.vnode || document.createElement(vnode.sel!);
|
const target = __owl__.vnode || document.createElement(vnode.sel!);
|
||||||
if (this.__owl__.classObj) {
|
|
||||||
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, this.__owl__.classObj);
|
|
||||||
}
|
|
||||||
__owl__.vnode = patch(target, vnode);
|
__owl__.vnode = patch(target, vnode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,6 +551,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
__owl__.render = qweb.render.bind(qweb, this.template);
|
__owl__.render = qweb.render.bind(qweb, this.template);
|
||||||
this.__observeState();
|
this.__observeState();
|
||||||
|
|
||||||
return this.__render(false, [], scope, vars);
|
return this.__render(false, [], scope, vars);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,9 +564,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
const patch: any[] = [this];
|
const patch: any[] = [this];
|
||||||
if (__owl__.isMounted) {
|
|
||||||
patchQueue.push(patch);
|
patchQueue.push(patch);
|
||||||
}
|
|
||||||
if (__owl__.observer) {
|
if (__owl__.observer) {
|
||||||
__owl__.observer.allowMutations = false;
|
__owl__.observer.allowMutations = false;
|
||||||
}
|
}
|
||||||
@@ -557,6 +594,14 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
// parent component. With this, we make sure that the parent component will be
|
// parent component. With this, we make sure that the parent component will be
|
||||||
// able to patch itself properly after
|
// able to patch itself properly after
|
||||||
vnode.key = __owl__.id;
|
vnode.key = __owl__.id;
|
||||||
|
|
||||||
|
// we applly here the class information described on the component by the
|
||||||
|
// template (so, something like <MyComponent class="..."/>) to the actual
|
||||||
|
// root vnode
|
||||||
|
if (__owl__.classObj) {
|
||||||
|
vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj);
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.all(promises).then(() => vnode);
|
return Promise.all(promises).then(() => vnode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,6 +631,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the observe feature on the state. We only create an observer if
|
||||||
|
* there is some state to be observed.
|
||||||
|
*/
|
||||||
__observeState() {
|
__observeState() {
|
||||||
if (this.state) {
|
if (this.state) {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
@@ -646,13 +695,21 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
// Error handling
|
// Error handling
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
function errorHandler(error, component) {
|
/**
|
||||||
|
* This is the global error handler for errors occurring in Owl main lifecycle
|
||||||
|
* methods. Caught errors are triggered on the QWeb instance, and are
|
||||||
|
* potentially given to some parent component which implements `catchError`.
|
||||||
|
*
|
||||||
|
* If there are no such component, we destroy everything. This is better than
|
||||||
|
* being in a corrupted state.
|
||||||
|
*/
|
||||||
|
function errorHandler(error: Error, component: Component<any, any, any>) {
|
||||||
let canCatch = false;
|
let canCatch = false;
|
||||||
let qweb = component.env.qweb;
|
let qweb = component.env.qweb;
|
||||||
let root = component;
|
let root = component;
|
||||||
while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) {
|
while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) {
|
||||||
root = component;
|
root = component;
|
||||||
component = component.__owl__.parent;
|
component = component.__owl__.parent!;
|
||||||
}
|
}
|
||||||
console.error(error);
|
console.error(error);
|
||||||
// we trigger error on QWeb so it can be logged/handled
|
// we trigger error on QWeb so it can be logged/handled
|
||||||
|
|||||||
@@ -454,7 +454,14 @@ QWeb.addDirective({
|
|||||||
|
|
||||||
ctx.addElse();
|
ctx.addElse();
|
||||||
// need to update component
|
// need to update component
|
||||||
const patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
|
let patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
|
||||||
|
if (keepAlive) {
|
||||||
|
// if we have t-keepalive="1", the component could be unmounted, but then
|
||||||
|
// we __updateProps is called. This is ok, but we do not want to call
|
||||||
|
// the willPatch/patched hooks of the component in this case, so we
|
||||||
|
// disable the patch queue
|
||||||
|
patchQueueCode = `w${componentID}.__owl__.isMounted ? ${patchQueueCode} : []`;
|
||||||
|
}
|
||||||
if (QWeb.dev) {
|
if (QWeb.dev) {
|
||||||
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
|
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -207,12 +207,12 @@ export class QWeb extends EventBus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load templates from a xml (as a string). This will look up for the first
|
* Load templates from a xml (as a string or xml document). This will look up
|
||||||
* <templates> tag, and will consider each child of this as a template, with
|
* for the first <templates> tag, and will consider each child of this as a
|
||||||
* the name given by the t-name attribute.
|
* template, with the name given by the t-name attribute.
|
||||||
*/
|
*/
|
||||||
addTemplates(xmlstr: string) {
|
addTemplates(xmlstr: string | Document) {
|
||||||
const doc = parseXML(xmlstr);
|
const doc = typeof xmlstr === 'string' ? parseXML(xmlstr) : xmlstr;
|
||||||
const templates = doc.getElementsByTagName("templates")[0];
|
const templates = doc.getElementsByTagName("templates")[0];
|
||||||
if (!templates) {
|
if (!templates) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -12,19 +12,20 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
|
|||||||
return env.store;
|
return env.store;
|
||||||
}
|
}
|
||||||
|
|
||||||
hashFunction: HashFunction = ({ storeProps }, options) => {
|
storeProps: any;
|
||||||
const observer = (this.__owl__ as any).store.observer;
|
|
||||||
let refFunction = this.deep ? observer.deepRevNumber : observer.revNumber;
|
hashFunction: HashFunction = (storeProps, options) => {
|
||||||
if ("__owl__" in storeProps) {
|
const revFn = (this.__owl__ as any).revFn;
|
||||||
return refFunction.call(observer, storeProps);
|
const rev = revFn(storeProps);
|
||||||
|
if (rev > 0) {
|
||||||
|
return rev;
|
||||||
}
|
}
|
||||||
const { currentStoreProps } = options;
|
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let key in storeProps) {
|
for (let key in storeProps) {
|
||||||
const val = storeProps[key];
|
const val = storeProps[key];
|
||||||
const hashVal = refFunction.call(observer, val);
|
const hashVal = revFn(val);
|
||||||
if (hashVal === 0) {
|
if (hashVal === 0) {
|
||||||
if (val !== currentStoreProps[key]) {
|
if (val !== options.prevStoreProps[key]) {
|
||||||
options.didChange = true;
|
options.didChange = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -37,32 +38,30 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
|
|||||||
static mapStoreToProps(storeState, ownProps, getters) {
|
static mapStoreToProps(storeState, ownProps, getters) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
constructor(parent, props?: any) {
|
|
||||||
super(parent, props);
|
|
||||||
|
|
||||||
|
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(
|
||||||
|
scope?: Object,
|
||||||
|
vars?: any
|
||||||
|
): ReturnType<Component<any, any, any>["__prepareAndRender"]> {
|
||||||
const store = this.getStore(this.env);
|
const store = this.getStore(this.env);
|
||||||
const ownProps = this.props || {};
|
const ownProps = this.props || {};
|
||||||
const storeProps = (<any>this.constructor).mapStoreToProps(
|
this.storeProps = (<any>this.constructor).mapStoreToProps(store.state, ownProps, store.getters);
|
||||||
store.state,
|
const observer = store.observer;
|
||||||
ownProps,
|
const revFn = this.deep ? observer.deepRevNumber : observer.revNumber;
|
||||||
store.getters
|
(this.__owl__ as any).store = store;
|
||||||
);
|
(this.__owl__ as any).revFn = revFn.bind(observer);
|
||||||
const mergedProps = Object.assign({}, ownProps, storeProps);
|
(this.__owl__ as any).storeHash = this.hashFunction(this.storeProps, {
|
||||||
|
prevStoreProps: this.storeProps
|
||||||
this.props = mergedProps;
|
});
|
||||||
|
(this.__owl__ as any).rev = observer.rev;
|
||||||
(<any>this.__owl__).ownProps = ownProps;
|
return super.__prepareAndRender(scope, vars);
|
||||||
(<any>this.__owl__).currentStoreProps = storeProps;
|
|
||||||
(<any>this.__owl__).store = store;
|
|
||||||
(<any>this.__owl__).storeHash = this.hashFunction(
|
|
||||||
{
|
|
||||||
state: store.state,
|
|
||||||
storeProps: storeProps
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currentStoreProps: storeProps
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* We do not use the mounted hook here for a subtle reason: we want the
|
* We do not use the mounted hook here for a subtle reason: we want the
|
||||||
@@ -70,56 +69,51 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
|
|||||||
* if we use the mounted hook, this will be done in the reverse order.
|
* if we use the mounted hook, this will be done in the reverse order.
|
||||||
*/
|
*/
|
||||||
__callMounted() {
|
__callMounted() {
|
||||||
(<any>this.__owl__).store.on("update", this, this.__checkUpdate);
|
(this.__owl__ as any).store.on("update", this, this.__checkUpdate);
|
||||||
super.__callMounted();
|
super.__callMounted();
|
||||||
}
|
}
|
||||||
willUnmount() {
|
__callWillUnmount() {
|
||||||
(<any>this.__owl__).store.off("update", this);
|
(this.__owl__ as any).store.off("update", this);
|
||||||
super.willUnmount();
|
super.__callWillUnmount();
|
||||||
|
}
|
||||||
|
__destroy(parent: any) {
|
||||||
|
(this.__owl__ as any).store.off("update", this);
|
||||||
|
super.__destroy(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
async __checkUpdate(updateId) {
|
async __updateProps(nextProps: P, f, p, s, v) {
|
||||||
if (updateId === (<any>this.__owl__).currentUpdateId) {
|
this.__updateStoreProps(nextProps);
|
||||||
|
return super.__updateProps(nextProps, f, p, s, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
__updateStoreProps(nextProps): boolean {
|
||||||
|
const store = (this.__owl__ as any).store;
|
||||||
|
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 !== (this.__owl__ as any).storeHash) {
|
||||||
|
(this.__owl__ as any).storeHash = storeHash;
|
||||||
|
didChange = true;
|
||||||
|
}
|
||||||
|
(this.__owl__ as any).rev = store.observer.rev;
|
||||||
|
return didChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
async __checkUpdate() {
|
||||||
|
const observer = (this.__owl__ as any).store.observer;
|
||||||
|
if (observer.rev === (this.__owl__ as any).rev) {
|
||||||
|
// update was already done by updateProps, from parent
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ownProps = (<any>this.__owl__).ownProps;
|
const didChange = this.__updateStoreProps(this.props);
|
||||||
const storeProps = (<any>this.constructor).mapStoreToProps(
|
|
||||||
(<any>this.__owl__).store.state,
|
|
||||||
ownProps,
|
|
||||||
(<any>this.__owl__).store.getters
|
|
||||||
);
|
|
||||||
const options: any = {
|
|
||||||
currentStoreProps: (<any>this.__owl__).currentStoreProps
|
|
||||||
};
|
|
||||||
const storeHash = this.hashFunction(
|
|
||||||
{
|
|
||||||
state: (<any>this.__owl__).store.state,
|
|
||||||
storeProps: storeProps
|
|
||||||
},
|
|
||||||
options
|
|
||||||
);
|
|
||||||
let didChange = options.didChange;
|
|
||||||
if (storeHash !== (<any>this.__owl__).storeHash) {
|
|
||||||
didChange = true;
|
|
||||||
(<any>this.__owl__).storeHash = storeHash;
|
|
||||||
}
|
|
||||||
if (didChange) {
|
if (didChange) {
|
||||||
(<any>this.__owl__).currentStoreProps = storeProps;
|
return this.render();
|
||||||
await this.__updateProps(ownProps, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
__updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
|
|
||||||
const __owl__ = <any>this.__owl__;
|
|
||||||
__owl__.currentUpdateId = __owl__.store._updateId;
|
|
||||||
if (__owl__.ownProps !== nextProps) {
|
|
||||||
__owl__.currentStoreProps = (<any>this.constructor).mapStoreToProps(
|
|
||||||
__owl__.store.state,
|
|
||||||
nextProps,
|
|
||||||
__owl__.store.getters
|
|
||||||
);
|
|
||||||
}
|
|
||||||
__owl__.ownProps = nextProps;
|
|
||||||
const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
|
|
||||||
return super.__updateProps(mergedProps, forceUpdate, patchQueue);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-66
@@ -11,7 +11,7 @@ import { Observer } from "../core/observer";
|
|||||||
*
|
*
|
||||||
* The Owl store is our answer to the problem of managing complex state across
|
* 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
|
* components. The main idea is that the store owns some state, allow external
|
||||||
* code to modify it through actions/mutations, and for each state changes,
|
* code to modify it through actions, and for each state changes,
|
||||||
* connected component will be notified, and updated if necessary.
|
* connected component will be notified, and updated if necessary.
|
||||||
*
|
*
|
||||||
* Note that this code is partly inspired by VueX and React/Redux
|
* Note that this code is partly inspired by VueX and React/Redux
|
||||||
@@ -21,16 +21,14 @@ import { Observer } from "../core/observer";
|
|||||||
// Store Definition
|
// Store Definition
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
type Mutation = ({ state, commit, getters }, ...payload: any) => void;
|
export type Action = ({ state, dispatch, env, getters }, ...payload: any) => any;
|
||||||
type Action = ({ commit, state, dispatch, env, getters }, ...payload: any) => void;
|
export type Getter = ({ state: any, getters }, payload?) => any;
|
||||||
type Getter = ({ state, getters }, payload) => any;
|
|
||||||
|
|
||||||
interface StoreConfig {
|
interface StoreConfig {
|
||||||
env?: Env;
|
env?: Env;
|
||||||
state?: any;
|
state?: any;
|
||||||
actions?: { [name: string]: Action };
|
actions?: { [name: string]: Action };
|
||||||
getters?: { [name: string]: Getter };
|
getters?: { [name: string]: Getter };
|
||||||
mutations?: { [name: string]: Mutation };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StoreOption {
|
interface StoreOption {
|
||||||
@@ -41,46 +39,28 @@ export class Store extends EventBus {
|
|||||||
state: any;
|
state: any;
|
||||||
actions: any;
|
actions: any;
|
||||||
mutations: any;
|
mutations: any;
|
||||||
_commitLevel: number = 0;
|
|
||||||
history: any[] = [];
|
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
env: any;
|
env: any;
|
||||||
observer: Observer;
|
observer: Observer;
|
||||||
getters: { [name: string]: (payload?) => any };
|
getters: { [name: string]: (payload?) => any };
|
||||||
_gettersCache: { [name: string]: {} };
|
|
||||||
_updateId: number = 1;
|
|
||||||
|
|
||||||
constructor(config: StoreConfig, options: StoreOption = {}) {
|
constructor(config: StoreConfig, options: StoreOption = {}) {
|
||||||
super();
|
super();
|
||||||
this.debug = options.debug || false;
|
this.debug = options.debug || false;
|
||||||
this.actions = config.actions;
|
this.actions = config.actions;
|
||||||
this.mutations = config.mutations;
|
|
||||||
this.env = config.env;
|
this.env = config.env;
|
||||||
this.observer = new Observer();
|
this.observer = new Observer();
|
||||||
this.observer.notifyCB = this.__notifyComponents.bind(this);
|
this.observer.notifyCB = this.__notifyComponents.bind(this);
|
||||||
this.observer.allowMutations = false;
|
|
||||||
this.state = this.observer.observe(config.state || {});
|
this.state = this.observer.observe(config.state || {});
|
||||||
this.getters = {};
|
this.getters = {};
|
||||||
this._gettersCache = {};
|
if (config.getters) {
|
||||||
|
const firstArg = {
|
||||||
if (this.debug) {
|
state: this.state,
|
||||||
this.history.push({ state: this.state });
|
getters: this.getters,
|
||||||
}
|
|
||||||
|
|
||||||
const cTypes = ["undefined", "number", "string"];
|
|
||||||
for (let entry of Object.entries(config.getters || {})) {
|
|
||||||
const name: string = entry[0];
|
|
||||||
const func: (...any) => any = entry[1];
|
|
||||||
this.getters[name] = payload => {
|
|
||||||
if (this._commitLevel === 0 && cTypes.indexOf(typeof payload) >= 0) {
|
|
||||||
this._gettersCache[name] = this._gettersCache[name] || {};
|
|
||||||
this._gettersCache[name][payload] =
|
|
||||||
this._gettersCache[name][payload] ||
|
|
||||||
func({ state: this.state, getters: this.getters }, payload);
|
|
||||||
return this._gettersCache[name][payload];
|
|
||||||
}
|
|
||||||
return func({ state: this.state, getters: this.getters }, payload);
|
|
||||||
};
|
};
|
||||||
|
for (let g in config.getters) {
|
||||||
|
this.getters[g] = config.getters[g].bind(this, firstArg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +70,6 @@ export class Store extends EventBus {
|
|||||||
}
|
}
|
||||||
const result = this.actions[action](
|
const result = this.actions[action](
|
||||||
{
|
{
|
||||||
commit: this.commit.bind(this),
|
|
||||||
dispatch: this.dispatch.bind(this),
|
dispatch: this.dispatch.bind(this),
|
||||||
env: this.env,
|
env: this.env,
|
||||||
state: this.state,
|
state: this.state,
|
||||||
@@ -101,37 +80,6 @@ export class Store extends EventBus {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
commit(type: string, ...payload: any): any {
|
|
||||||
if (!this.mutations[type]) {
|
|
||||||
throw new Error(`[Error] mutation ${type} is undefined`);
|
|
||||||
}
|
|
||||||
this._commitLevel++;
|
|
||||||
this.observer.allowMutations = true;
|
|
||||||
|
|
||||||
const res = this.mutations[type].call(
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
commit: this.commit.bind(this),
|
|
||||||
state: this.state,
|
|
||||||
getters: this.getters
|
|
||||||
},
|
|
||||||
...payload
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this._commitLevel === 1) {
|
|
||||||
this.observer.allowMutations = false;
|
|
||||||
if (this.debug) {
|
|
||||||
this.history.push({
|
|
||||||
state: this.state,
|
|
||||||
mutation: type,
|
|
||||||
payload: [...payload]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._commitLevel--;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instead of using trigger to emit an update event, we actually implement
|
* 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
|
* our own function to do that. The reason is that we need to be smarter than
|
||||||
@@ -147,15 +95,12 @@ export class Store extends EventBus {
|
|||||||
* updating all widgets concurrently, except for parents/children.
|
* updating all widgets concurrently, except for parents/children.
|
||||||
*/
|
*/
|
||||||
async __notifyComponents() {
|
async __notifyComponents() {
|
||||||
this._updateId++;
|
|
||||||
const current = this._updateId;
|
|
||||||
this._gettersCache = {};
|
|
||||||
const subs = this.subscriptions.update || [];
|
const subs = this.subscriptions.update || [];
|
||||||
for (let i = 0, iLen = subs.length; i < iLen; i++) {
|
for (let i = 0, iLen = subs.length; i < iLen; i++) {
|
||||||
const sub = subs[i];
|
const sub = subs[i];
|
||||||
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
|
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
|
||||||
if (shouldCallback) {
|
if (shouldCallback) {
|
||||||
await sub.callback.call(sub.owner, current);
|
await sub.callback.call(sub.owner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -469,10 +469,6 @@ interface DOMAPI {
|
|||||||
nextSibling: (node: Node) => Node;
|
nextSibling: (node: Node) => Node;
|
||||||
tagName: (elm: Element) => string;
|
tagName: (elm: Element) => string;
|
||||||
setTextContent: (node: Node, text: string | null) => void;
|
setTextContent: (node: Node, text: string | null) => void;
|
||||||
getTextContent: (node: Node) => string | null;
|
|
||||||
isElement: (node: Node) => node is Element;
|
|
||||||
isText: (node: Node) => node is Text;
|
|
||||||
isComment: (node: Node) => node is Comment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createElement(tagName: any): HTMLElement {
|
function createElement(tagName: any): HTMLElement {
|
||||||
@@ -519,22 +515,6 @@ function setTextContent(node: Node, text: string | null): void {
|
|||||||
node.textContent = text;
|
node.textContent = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTextContent(node: Node): string | null {
|
|
||||||
return node.textContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isElement(node: Node): node is Element {
|
|
||||||
return node.nodeType === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isText(node: Node): node is Text {
|
|
||||||
return node.nodeType === 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isComment(node: Node): node is Comment {
|
|
||||||
return node.nodeType === 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
const htmlDomApi = {
|
const htmlDomApi = {
|
||||||
createElement,
|
createElement,
|
||||||
createElementNS,
|
createElementNS,
|
||||||
@@ -547,10 +527,6 @@ const htmlDomApi = {
|
|||||||
nextSibling,
|
nextSibling,
|
||||||
tagName,
|
tagName,
|
||||||
setTextContent,
|
setTextContent,
|
||||||
getTextContent,
|
|
||||||
isElement,
|
|
||||||
isText,
|
|
||||||
isComment
|
|
||||||
} as DOMAPI;
|
} as DOMAPI;
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -447,6 +447,49 @@ exports[`class and style attributes with t-component t-att-class is properly add
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`composition sub components dom state with t-keepalive is preserved 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
let QWeb = this.constructor;
|
||||||
|
let parent = context;
|
||||||
|
let owner = context;
|
||||||
|
var h = this.h;
|
||||||
|
let c1 = [], p1 = {key:1};
|
||||||
|
var vn1 = h('div', p1, c1);
|
||||||
|
if (context['state'].ok) {
|
||||||
|
//COMPONENT
|
||||||
|
let def3;
|
||||||
|
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||||
|
let _2_index = c1.length;
|
||||||
|
c1.push(null);
|
||||||
|
let props4 = {};
|
||||||
|
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||||
|
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||||
|
def3 = w4.__owl__.renderPromise;
|
||||||
|
} else {
|
||||||
|
w4.destroy();
|
||||||
|
w4 = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!w4) {
|
||||||
|
let componentKey4 = \`InputWidget\`;
|
||||||
|
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||||
|
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||||
|
w4 = new W4(parent, props4);
|
||||||
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
|
def3 = w4.__prepare();
|
||||||
|
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.unmount();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
|
} else {
|
||||||
|
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, w4.__owl__.isMounted ? extra.patchQueue : []);
|
||||||
|
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w4.el,vn.elm);vn.elm=w4.el;w4.__remount();};c1[_2_index]=pvnode;});
|
||||||
|
}
|
||||||
|
extra.promises.push(def3);
|
||||||
|
}
|
||||||
|
return vn1;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`composition sub components with some state rendered in a loop 1`] = `
|
exports[`composition sub components with some state rendered in a loop 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
@@ -587,6 +630,49 @@ exports[`composition t-component with dynamic value 2 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`lifecycle hooks willPatch/patched hook with t-keepalive 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
let QWeb = this.constructor;
|
||||||
|
let parent = context;
|
||||||
|
let owner = context;
|
||||||
|
var h = this.h;
|
||||||
|
let c1 = [], p1 = {key:1};
|
||||||
|
var vn1 = h('div', p1, c1);
|
||||||
|
if (context['state'].flag) {
|
||||||
|
//COMPONENT
|
||||||
|
let def3;
|
||||||
|
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||||
|
let _2_index = c1.length;
|
||||||
|
c1.push(null);
|
||||||
|
let props4 = {v:context['state'].n};
|
||||||
|
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||||
|
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||||
|
def3 = w4.__owl__.renderPromise;
|
||||||
|
} else {
|
||||||
|
w4.destroy();
|
||||||
|
w4 = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!w4) {
|
||||||
|
let componentKey4 = \`ChildWidget\`;
|
||||||
|
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||||
|
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||||
|
w4 = new W4(parent, props4);
|
||||||
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
|
def3 = w4.__prepare();
|
||||||
|
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.unmount();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
|
} else {
|
||||||
|
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, w4.__owl__.isMounted ? extra.patchQueue : []);
|
||||||
|
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w4.el,vn.elm);vn.elm=w4.el;w4.__remount();};c1[_2_index]=pvnode;});
|
||||||
|
}
|
||||||
|
extra.promises.push(def3);
|
||||||
|
}
|
||||||
|
return vn1;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`other directives with t-component t-on with handler bound to argument 1`] = `
|
exports[`other directives with t-component t-on with handler bound to argument 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`default props default values are also set whenever component is updated 1`] = `"<div>1</div>"`;
|
||||||
|
|
||||||
|
exports[`default props default values are also set whenever component is updated 2`] = `"<div>4</div>"`;
|
||||||
|
|
||||||
exports[`props validation props are validated in dev mode (code snapshot) 1`] = `
|
exports[`props validation props are validated in dev mode (code snapshot) 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -733,15 +733,16 @@ describe("lifecycle hooks", () => {
|
|||||||
// we make sure here that willPatch/patched is only called if widget is in
|
// we make sure here that willPatch/patched is only called if widget is in
|
||||||
// dom, mounted
|
// dom, mounted
|
||||||
const steps: string[] = [];
|
const steps: string[] = [];
|
||||||
env.qweb.addTemplate(
|
env.qweb.addTemplates(`
|
||||||
"ParentWidget",
|
<templates>
|
||||||
`
|
<div t-name="ParentWidget">
|
||||||
<div>
|
<ChildWidget t-if="state.flag" v="state.n" t-keepalive="1"/>
|
||||||
<t t-if="state.flag" t-component="child" v="state.n" t-keepalive="1"/>
|
</div>
|
||||||
</div>`
|
</templates>
|
||||||
);
|
`);
|
||||||
|
|
||||||
class ParentWidget extends Widget {
|
class ParentWidget extends Widget {
|
||||||
components = { child: ChildWidget };
|
components = { ChildWidget };
|
||||||
state = { n: 1, flag: true };
|
state = { n: 1, flag: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -761,6 +762,8 @@ describe("lifecycle hooks", () => {
|
|||||||
}
|
}
|
||||||
const widget = new ParentWidget(env);
|
const widget = new ParentWidget(env);
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
|
|
||||||
|
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||||
expect(steps).toEqual(["child:mounted"]);
|
expect(steps).toEqual(["child:mounted"]);
|
||||||
widget.state.flag = false;
|
widget.state.flag = false;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@@ -1124,6 +1127,7 @@ describe("composition", () => {
|
|||||||
const input2 = fixture.getElementsByTagName("input")[0];
|
const input2 = fixture.getElementsByTagName("input")[0];
|
||||||
expect(input).toBe(input2);
|
expect(input).toBe(input2);
|
||||||
expect(input2.value).toBe("test");
|
expect(input2.value).toBe("test");
|
||||||
|
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("sub widget with t-ref and t-keepalive", async () => {
|
test("sub widget with t-ref and t-keepalive", async () => {
|
||||||
@@ -4010,4 +4014,74 @@ describe("unmounting and remounting", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<div>Hey</div>");
|
expect(fixture.innerHTML).toBe("<div>Hey</div>");
|
||||||
expect(steps).toEqual(["willstart", "mounted"]);
|
expect(steps).toEqual(["willstart", "mounted"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("state changes in willUnmount do not trigger rerender", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="Parent">
|
||||||
|
<Child t-if="state.flag" val="state.val"/>
|
||||||
|
</div>
|
||||||
|
<span t-name="Child"><t t-esc="props.val"/><t t-esc="state.n"/></span>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class Child extends Widget {
|
||||||
|
state = { n: 2 };
|
||||||
|
__render(a, b, c, d) {
|
||||||
|
steps.push("render");
|
||||||
|
return super.__render(a, b, c, d);
|
||||||
|
}
|
||||||
|
willPatch() {
|
||||||
|
steps.push("willPatch");
|
||||||
|
}
|
||||||
|
patched() {
|
||||||
|
steps.push("patched");
|
||||||
|
}
|
||||||
|
|
||||||
|
willUnmount() {
|
||||||
|
steps.push("willUnmount");
|
||||||
|
this.state.n = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Widget {
|
||||||
|
components = { Child };
|
||||||
|
state = { val: 1, flag: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = new Parent(env);
|
||||||
|
await widget.mount(fixture);
|
||||||
|
expect(steps).toEqual(["render"]);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
|
||||||
|
widget.state.flag = false;
|
||||||
|
await nextTick();
|
||||||
|
// we make sure here that no call to __render is done
|
||||||
|
expect(steps).toEqual(["render", "willUnmount"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("state changes in willUnmount will be applied on remount", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="TestWidget"><t t-esc="state.val"/></div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class TestWidget extends Widget {
|
||||||
|
state = { val: 1 };
|
||||||
|
willUnmount() {
|
||||||
|
this.state.val = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = new TestWidget(env);
|
||||||
|
await widget.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>1</div>");
|
||||||
|
widget.unmount();
|
||||||
|
expect(fixture.innerHTML).toBe("");
|
||||||
|
await widget.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>1</div>");
|
||||||
|
widget.unmount();
|
||||||
|
await widget.mount(fixture, true);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>3</div>");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -327,9 +327,17 @@ describe("default props", () => {
|
|||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
static defaultProps = { p: 4 };
|
static defaultProps = { p: 4 };
|
||||||
}
|
}
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="TestWidget"><t t-esc="props.p"/></div>
|
||||||
|
</templates>`);
|
||||||
|
|
||||||
const w = new TestWidget(env, { p: 1 });
|
const w = new TestWidget(env, { p: 1 });
|
||||||
|
await w.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
await w.__updateProps({});
|
await w.__updateProps({});
|
||||||
|
await w.render();
|
||||||
expect(w.props.p).toBe(4);
|
expect(w.props.p).toBe(4);
|
||||||
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, Env } from "../../src/component/component";
|
import { Component, Env } from "../../src/component/component";
|
||||||
import { ConnectedComponent } from "../../src/store/connected_component";
|
import { ConnectedComponent } from "../../src/store/connected_component";
|
||||||
import { Store } from "../../src/store/store";
|
import { Store } from "../../src/store/store";
|
||||||
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
import { makeTestEnv, makeTestFixture, nextTick, makeDeferred } from "../helpers";
|
||||||
|
|
||||||
describe("connecting a component to store", () => {
|
describe("connecting a component to store", () => {
|
||||||
let fixture: HTMLElement;
|
let fixture: HTMLElement;
|
||||||
@@ -20,7 +20,7 @@ describe("connecting a component to store", () => {
|
|||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<div t-name="App">
|
<div t-name="App">
|
||||||
<t t-foreach="props.todos" t-as="todo" >
|
<t t-foreach="storeProps.todos" t-as="todo" >
|
||||||
<Todo msg="todo.msg" t-key="todo"/>
|
<Todo msg="todo.msg" t-key="todo"/>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,19 +35,19 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
class Todo extends Component<any, any, any> {}
|
class Todo extends Component<any, any, any> {}
|
||||||
const state = { todos: [] };
|
const state = { todos: [] };
|
||||||
const mutations = {
|
const actions = {
|
||||||
addTodo({ state }, msg) {
|
addTodo({ state }, msg) {
|
||||||
state.todos.push({ msg });
|
state.todos.push({ msg });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({ state, mutations });
|
const store = new Store({ state, actions });
|
||||||
(<any>env).store = store;
|
(<any>env).store = store;
|
||||||
const app = new App(env);
|
const app = new App(env);
|
||||||
|
|
||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toMatchSnapshot();
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
|
|
||||||
store.commit("addTodo", "hello");
|
store.dispatch("addTodo", "hello");
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toMatchSnapshot();
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@@ -56,19 +56,19 @@ describe("connecting a component to store", () => {
|
|||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<div t-name="App">
|
<div t-name="App">
|
||||||
<span t-foreach="props.todos" t-as="todo" t-key="todo">
|
<span t-foreach="storeProps.todos" t-as="todo" t-key="todo">
|
||||||
<t t-esc="todo.title"/>
|
<t t-esc="todo.title"/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
const state = { todos: [{ title: "Kasteel" }] };
|
const state = { todos: [{ title: "Kasteel" }] };
|
||||||
const mutations = {
|
const actions = {
|
||||||
edit({ state }, title) {
|
edit({ state }, title) {
|
||||||
state.todos[0].title = title;
|
state.todos[0].title = title;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({ state, mutations });
|
const store = new Store({ state, actions });
|
||||||
|
|
||||||
class App extends ConnectedComponent<any, any, any> {
|
class App extends ConnectedComponent<any, any, any> {
|
||||||
static mapStoreToProps(s) {
|
static mapStoreToProps(s) {
|
||||||
@@ -94,7 +94,7 @@ describe("connecting a component to store", () => {
|
|||||||
expect(fixture.innerHTML).toMatchSnapshot();
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
expect(shallowFix.innerHTML).toMatchSnapshot();
|
expect(shallowFix.innerHTML).toMatchSnapshot();
|
||||||
|
|
||||||
store.commit("edit", "Bertinchamps");
|
store.dispatch("edit", "Bertinchamps");
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toMatchSnapshot();
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
expect(shallowFix.innerHTML).toMatchSnapshot();
|
expect(shallowFix.innerHTML).toMatchSnapshot();
|
||||||
@@ -104,7 +104,7 @@ describe("connecting a component to store", () => {
|
|||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<div t-name="App">
|
<div t-name="App">
|
||||||
<t t-foreach="props.todos" t-as="todo">
|
<t t-foreach="storeProps.todos" t-as="todo">
|
||||||
<Todo msg="todo.msg" t-key="todo" />
|
<Todo msg="todo.msg" t-key="todo" />
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,10 +113,9 @@ describe("connecting a component to store", () => {
|
|||||||
`);
|
`);
|
||||||
class Todo extends Component<any, any, any> {}
|
class Todo extends Component<any, any, any> {}
|
||||||
|
|
||||||
(<any>env).store = new Store({});
|
|
||||||
const store = new Store({
|
const store = new Store({
|
||||||
state: { todos: [] },
|
state: { todos: [] },
|
||||||
mutations: {
|
actions: {
|
||||||
addTodo({ state }, msg) {
|
addTodo({ state }, msg) {
|
||||||
state.todos.push({ msg });
|
state.todos.push({ msg });
|
||||||
}
|
}
|
||||||
@@ -136,11 +135,48 @@ describe("connecting a component to store", () => {
|
|||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toMatchSnapshot();
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
|
|
||||||
(<any>app.__owl__).store.commit("addTodo", "hello");
|
(<any>app.__owl__).store.dispatch("addTodo", "hello");
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toMatchSnapshot();
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can dispatch actions from a connected component", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="App">
|
||||||
|
<button t-on-click="dispatch('inc')">Inc</button>
|
||||||
|
<span><t t-esc="storeProps.value"/></span>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const store = new Store({
|
||||||
|
state: { value: 1 },
|
||||||
|
actions: {
|
||||||
|
inc({ state }) {
|
||||||
|
state.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(<any>env).store = store;
|
||||||
|
|
||||||
|
class App extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps(s) {
|
||||||
|
return { value: s.value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const app = new App(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><button>Inc</button><span>1</span></div>");
|
||||||
|
|
||||||
|
const button = (<HTMLElement>app.el).getElementsByTagName("button")[0];
|
||||||
|
await button.click();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe("<div><button>Inc</button><span>2</span></div>");
|
||||||
|
});
|
||||||
|
|
||||||
test("connected child components with custom hooks", async () => {
|
test("connected child components with custom hooks", async () => {
|
||||||
let steps: any = [];
|
let steps: any = [];
|
||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
@@ -187,18 +223,18 @@ describe("connecting a component to store", () => {
|
|||||||
test("mapStoreToProps receives ownprops as second argument", async () => {
|
test("mapStoreToProps receives ownprops as second argument", async () => {
|
||||||
const state = { todos: [{ id: 1, text: "jupiler" }] };
|
const state = { todos: [{ id: 1, text: "jupiler" }] };
|
||||||
let nextId = 2;
|
let nextId = 2;
|
||||||
const mutations = {
|
const actions = {
|
||||||
addTodo({ state }, text) {
|
addTodo({ state }, text) {
|
||||||
state.todos.push({ text, id: nextId++ });
|
state.todos.push({ text, id: nextId++ });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({ state, mutations });
|
const store = new Store({ state, actions });
|
||||||
|
|
||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<span t-name="TodoItem"><t t-esc="props.text"/></span>
|
<span t-name="TodoItem"><t t-esc="storeProps.text"/></span>
|
||||||
<div t-name="TodoList">
|
<div t-name="TodoList">
|
||||||
<t t-foreach="props.todos" t-as="todo">
|
<t t-foreach="storeProps.todos" t-as="todo">
|
||||||
<TodoItem id="todo.id" t-key="todo.id"/>
|
<TodoItem id="todo.id" t-key="todo.id"/>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
@@ -224,7 +260,7 @@ describe("connecting a component to store", () => {
|
|||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
||||||
|
|
||||||
store.commit("addTodo", "hoegaarden");
|
store.dispatch("addTodo", "hoegaarden");
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>hoegaarden</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>hoegaarden</span></div>");
|
||||||
});
|
});
|
||||||
@@ -247,11 +283,11 @@ describe("connecting a component to store", () => {
|
|||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<div t-name="TodoItem">
|
<div t-name="TodoItem">
|
||||||
<span><t t-esc="props.activeTodoText"/></span>
|
<span><t t-esc="storeProps.activeTodoText"/></span>
|
||||||
<span><t t-esc="props.importantTodoText"/></span>
|
<span><t t-esc="storeProps.importantTodoText"/></span>
|
||||||
</div>
|
</div>
|
||||||
<div t-name="TodoList">
|
<div t-name="TodoList">
|
||||||
<t t-foreach="props.todos" t-as="todo">
|
<t t-foreach="storeProps.todos" t-as="todo">
|
||||||
<TodoItem id="todo.id" t-key="todo.id"/>
|
<TodoItem id="todo.id" t-key="todo.id"/>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
@@ -287,7 +323,7 @@ describe("connecting a component to store", () => {
|
|||||||
test("connected component is updated when props are updated", async () => {
|
test("connected component is updated when props are updated", async () => {
|
||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<span t-name="Beer"><t t-esc="props.name"/></span>
|
<span t-name="Beer"><t t-esc="storeProps.name"/></span>
|
||||||
<div t-name="App">
|
<div t-name="App">
|
||||||
<Beer id="state.beerId"/>
|
<Beer id="state.beerId"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -322,7 +358,7 @@ describe("connecting a component to store", () => {
|
|||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<div t-name="App">
|
<div t-name="App">
|
||||||
<span t-foreach="props.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span>
|
<span t-foreach="storeProps.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span>
|
||||||
</div>
|
</div>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
@@ -333,14 +369,14 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutations = {
|
const actions = {
|
||||||
addBeer({ state }, name) {
|
addBeer({ state }, name) {
|
||||||
state.beers.push({ name });
|
state.beers.push({ name });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = { beers: [{ name: "jupiler" }] };
|
const state = { beers: [{ name: "jupiler" }] };
|
||||||
const store = new Store({ state, mutations });
|
const store = new Store({ state, actions });
|
||||||
(<any>env).store = store;
|
(<any>env).store = store;
|
||||||
|
|
||||||
const app = new App(env);
|
const app = new App(env);
|
||||||
@@ -348,7 +384,7 @@ describe("connecting a component to store", () => {
|
|||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
||||||
|
|
||||||
store.commit("addBeer", "kwak");
|
store.dispatch("addBeer", "kwak");
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>kwak</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>kwak</span></div>");
|
||||||
});
|
});
|
||||||
@@ -357,9 +393,9 @@ describe("connecting a component to store", () => {
|
|||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<div t-name="Beer">
|
<div t-name="Beer">
|
||||||
<span>taster:<t t-esc="props.taster"/></span>
|
<span>taster:<t t-esc="storeProps.taster"/></span>
|
||||||
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
|
<span t-if="storeProps.selected">selected:<t t-esc="storeProps.selected.name"/></span>
|
||||||
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
|
<span t-if="storeProps.consumed">consumed:<t t-esc="storeProps.consumed.name"/></span>
|
||||||
</div>
|
</div>
|
||||||
<div t-name="App">
|
<div t-name="App">
|
||||||
<Beer id="state.beerId"/>
|
<Beer id="state.beerId"/>
|
||||||
@@ -382,7 +418,7 @@ describe("connecting a component to store", () => {
|
|||||||
state = { beerId: 0 };
|
state = { beerId: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutations = {
|
const actions = {
|
||||||
consume({ state }, beerId) {
|
consume({ state }, beerId) {
|
||||||
state.consumedID = beerId;
|
state.consumedID = beerId;
|
||||||
}
|
}
|
||||||
@@ -394,7 +430,7 @@ describe("connecting a component to store", () => {
|
|||||||
consumedID: null,
|
consumedID: null,
|
||||||
taster: "aaron"
|
taster: "aaron"
|
||||||
};
|
};
|
||||||
const store = new Store({ state, mutations });
|
const store = new Store({ state, actions });
|
||||||
(<any>env).store = store;
|
(<any>env).store = store;
|
||||||
const app = new App(env);
|
const app = new App(env);
|
||||||
|
|
||||||
@@ -407,7 +443,7 @@ describe("connecting a component to store", () => {
|
|||||||
"<div><div><span>taster:aaron</span><span>selected:jupiler</span></div></div>"
|
"<div><div><span>taster:aaron</span><span>selected:jupiler</span></div></div>"
|
||||||
);
|
);
|
||||||
|
|
||||||
store.commit("consume", 1);
|
store.dispatch("consume", 1);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe(
|
expect(fixture.innerHTML).toBe(
|
||||||
"<div><div><span>taster:aaron</span><span>selected:jupiler</span><span>consumed:jupiler</span></div></div>"
|
"<div><div><span>taster:aaron</span><span>selected:jupiler</span><span>consumed:jupiler</span></div></div>"
|
||||||
@@ -424,9 +460,9 @@ describe("connecting a component to store", () => {
|
|||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<div t-name="Beer">
|
<div t-name="Beer">
|
||||||
<span>taster:<t t-esc="props.taster"/></span>
|
<span>taster:<t t-esc="storeProps.taster"/></span>
|
||||||
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
|
<span t-if="storeProps.selected">selected:<t t-esc="storeProps.selected.name"/></span>
|
||||||
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
|
<span t-if="storeProps.consumed">consumed:<t t-esc="storeProps.consumed.name"/></span>
|
||||||
</div>
|
</div>
|
||||||
<div t-name="App">
|
<div t-name="App">
|
||||||
<Beer id="state.beerId"/>
|
<Beer id="state.beerId"/>
|
||||||
@@ -449,7 +485,7 @@ describe("connecting a component to store", () => {
|
|||||||
state = { beerId: 0 };
|
state = { beerId: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutations = {
|
const actions = {
|
||||||
changeTaster({ state }, newTaster) {
|
changeTaster({ state }, newTaster) {
|
||||||
state.taster = newTaster;
|
state.taster = newTaster;
|
||||||
},
|
},
|
||||||
@@ -467,7 +503,7 @@ describe("connecting a component to store", () => {
|
|||||||
consumedID: null,
|
consumedID: null,
|
||||||
taster: "aaron"
|
taster: "aaron"
|
||||||
};
|
};
|
||||||
const store = new Store({ state, mutations });
|
const store = new Store({ state, actions });
|
||||||
(<any>env).store = store;
|
(<any>env).store = store;
|
||||||
const app = new App(env);
|
const app = new App(env);
|
||||||
|
|
||||||
@@ -480,13 +516,13 @@ describe("connecting a component to store", () => {
|
|||||||
"<div><div><span>taster:aaron</span><span>selected:jupiler</span></div></div>"
|
"<div><div><span>taster:aaron</span><span>selected:jupiler</span></div></div>"
|
||||||
);
|
);
|
||||||
|
|
||||||
store.commit("renameBeer", { beerId: 1, name: "kwak" });
|
store.dispatch("renameBeer", { beerId: 1, name: "kwak" });
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe(
|
expect(fixture.innerHTML).toBe(
|
||||||
"<div><div><span>taster:aaron</span><span>selected:kwak</span></div></div>"
|
"<div><div><span>taster:aaron</span><span>selected:kwak</span></div></div>"
|
||||||
);
|
);
|
||||||
|
|
||||||
store.commit("consume", 1);
|
store.dispatch("consume", 1);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe(
|
expect(fixture.innerHTML).toBe(
|
||||||
"<div><div><span>taster:aaron</span><span>selected:kwak</span><span>consumed:kwak</span></div></div>"
|
"<div><div><span>taster:aaron</span><span>selected:kwak</span><span>consumed:kwak</span></div></div>"
|
||||||
@@ -498,13 +534,13 @@ describe("connecting a component to store", () => {
|
|||||||
"<div><div><span>taster:aaron</span><span>consumed:kwak</span></div></div>"
|
"<div><div><span>taster:aaron</span><span>consumed:kwak</span></div></div>"
|
||||||
);
|
);
|
||||||
|
|
||||||
store.commit("renameBeer", { beerId: 1, name: "jupiler" });
|
store.dispatch("renameBeer", { beerId: 1, name: "jupiler" });
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe(
|
expect(fixture.innerHTML).toBe(
|
||||||
"<div><div><span>taster:aaron</span><span>consumed:jupiler</span></div></div>"
|
"<div><div><span>taster:aaron</span><span>consumed:jupiler</span></div></div>"
|
||||||
);
|
);
|
||||||
|
|
||||||
store.commit("changeTaster", "matthieu");
|
store.dispatch("changeTaster", "matthieu");
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe(
|
expect(fixture.innerHTML).toBe(
|
||||||
"<div><div><span>taster:matthieu</span><span>consumed:jupiler</span></div></div>"
|
"<div><div><span>taster:matthieu</span><span>consumed:jupiler</span></div></div>"
|
||||||
@@ -517,9 +553,9 @@ describe("connecting a component to store", () => {
|
|||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<div t-name="Parent">
|
<div t-name="Parent">
|
||||||
<Child key="props.current"/>
|
<Child key="storeProps.current"/>
|
||||||
</div>
|
</div>
|
||||||
<span t-name="Child"><t t-esc="props.msg"/></span>
|
<span t-name="Child"><t t-esc="storeProps.msg"/></span>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -539,13 +575,13 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const state = { current: "a", msg: { a: "a", b: "b" } };
|
const state = { current: "a", msg: { a: "a", b: "b" } };
|
||||||
const mutations = {
|
const actions = {
|
||||||
setCurrent({ state }, c) {
|
setCurrent({ state }, c) {
|
||||||
state.current = c;
|
state.current = c;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const store = new Store({ state, mutations });
|
const store = new Store({ state, actions });
|
||||||
(<any>env).store = store;
|
(<any>env).store = store;
|
||||||
const app = new Parent(env);
|
const app = new Parent(env);
|
||||||
|
|
||||||
@@ -553,14 +589,74 @@ describe("connecting a component to store", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<div><span>a</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>a</span></div>");
|
||||||
expect(steps).toEqual(["parent", "child"]);
|
expect(steps).toEqual(["parent", "child"]);
|
||||||
|
|
||||||
store.commit("setCurrent", "b");
|
store.dispatch("setCurrent", "b");
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<div><span>b</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>b</span></div>");
|
||||||
expect(steps).toEqual(["parent", "child", "parent", "child"]);
|
expect(steps).toEqual(["parent", "child", "parent", "child"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("correct update order when parent/children are connectedddd", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
let def = makeDeferred();
|
||||||
|
def.resolve();
|
||||||
|
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="Parent">
|
||||||
|
<Child t-if="storeProps.flag" someId="storeProps.someId"/>
|
||||||
|
</div>
|
||||||
|
<span t-name="Child"><t t-esc="storeProps.msg"/></span>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class Parent extends ConnectedComponent<any, any, any> {
|
||||||
|
components = { Child };
|
||||||
|
static mapStoreToProps(s) {
|
||||||
|
steps.push("parent");
|
||||||
|
return { flag: s.flag, someId: s.someId };
|
||||||
|
}
|
||||||
|
async render(force) {
|
||||||
|
await def;
|
||||||
|
return super.render(force);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Child extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps(s, props) {
|
||||||
|
steps.push("child");
|
||||||
|
return { msg: s.messages[props.someId] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 () => {
|
test("connected parent/children: no double rendering", async () => {
|
||||||
const mutations = {
|
const actions = {
|
||||||
editTodo({ state }) {
|
editTodo({ state }) {
|
||||||
state.todos[1].title = "abc";
|
state.todos[1].title = "abc";
|
||||||
}
|
}
|
||||||
@@ -571,19 +667,19 @@ describe("connecting a component to store", () => {
|
|||||||
};
|
};
|
||||||
const store = new Store({
|
const store = new Store({
|
||||||
state,
|
state,
|
||||||
mutations
|
actions
|
||||||
});
|
});
|
||||||
|
|
||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<div t-name="TodoApp" class="todoapp">
|
<div t-name="TodoApp" class="todoapp">
|
||||||
<t t-foreach="Object.values(props.todos)" t-as="todo">
|
<t t-foreach="Object.values(storeProps.todos)" t-as="todo">
|
||||||
<TodoItem t-key="todo.id" id="todo.id"/>
|
<TodoItem t-key="todo.id" id="todo.id"/>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div t-name="TodoItem" class="todo">
|
<div t-name="TodoItem" class="todo">
|
||||||
<t t-esc="props.todo.title"/>
|
<t t-esc="storeProps.todo.title"/>
|
||||||
<button class="destroy" t-on-click="editTodo">x</button>
|
<button class="destroy" t-on-click="editTodo">x</button>
|
||||||
</div>
|
</div>
|
||||||
</templates>
|
</templates>
|
||||||
@@ -611,7 +707,7 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
editTodo() {
|
editTodo() {
|
||||||
this.env.store.commit("editTodo");
|
this.env.store.dispatch("editTodo");
|
||||||
}
|
}
|
||||||
__render(...args) {
|
__render(...args) {
|
||||||
renderCount++;
|
renderCount++;
|
||||||
@@ -639,7 +735,7 @@ describe("connecting a component to store", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("connected parent/children: no rendering if child is destroyed", async () => {
|
test("connected parent/children: no rendering if child is destroyed", async () => {
|
||||||
const mutations = {
|
const actions = {
|
||||||
removeTodo({ state }) {
|
removeTodo({ state }) {
|
||||||
delete state.todos[1];
|
delete state.todos[1];
|
||||||
}
|
}
|
||||||
@@ -650,19 +746,19 @@ describe("connecting a component to store", () => {
|
|||||||
};
|
};
|
||||||
const store = new Store({
|
const store = new Store({
|
||||||
state,
|
state,
|
||||||
mutations
|
actions
|
||||||
});
|
});
|
||||||
|
|
||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<div t-name="TodoApp" class="todoapp">
|
<div t-name="TodoApp" class="todoapp">
|
||||||
<t t-foreach="Object.values(props.todos)" t-as="todo">
|
<t t-foreach="Object.values(storeProps.todos)" t-as="todo">
|
||||||
<TodoItem t-key="todo.id" id="todo.id"/>
|
<TodoItem t-key="todo.id" id="todo.id"/>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div t-name="TodoItem" class="todo">
|
<div t-name="TodoItem" class="todo">
|
||||||
<t t-esc="props.todo.title"/>
|
<t t-esc="storeProps.todo.title"/>
|
||||||
<button class="destroy" t-on-click="removeTodo">x</button>
|
<button class="destroy" t-on-click="removeTodo">x</button>
|
||||||
</div>
|
</div>
|
||||||
</templates>
|
</templates>
|
||||||
@@ -690,7 +786,7 @@ describe("connecting a component to store", () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
removeTodo() {
|
removeTodo() {
|
||||||
this.env.store.commit("removeTodo");
|
this.env.store.dispatch("removeTodo");
|
||||||
}
|
}
|
||||||
__render(...args) {
|
__render(...args) {
|
||||||
renderCount++;
|
renderCount++;
|
||||||
@@ -720,7 +816,7 @@ describe("connecting a component to store", () => {
|
|||||||
|
|
||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<div t-name="App"><t t-esc="props.msg"/></div>
|
<div t-name="App"><t t-esc="storeProps.msg"/></div>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -737,20 +833,20 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const state = { msg: "a" };
|
const state = { msg: "a" };
|
||||||
const mutations = {
|
const actions = {
|
||||||
setMsg({ state }, c) {
|
setMsg({ state }, c) {
|
||||||
state.msg = c;
|
state.msg = c;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const store = new Store({ state, mutations });
|
const store = new Store({ state, actions });
|
||||||
(<any>env).store = store;
|
(<any>env).store = store;
|
||||||
const app = new App(env);
|
const app = new App(env);
|
||||||
|
|
||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div>a</div>");
|
expect(fixture.innerHTML).toBe("<div>a</div>");
|
||||||
|
|
||||||
store.commit("setMsg", "b");
|
store.dispatch("setMsg", "b");
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<div>b</div>");
|
expect(fixture.innerHTML).toBe("<div>b</div>");
|
||||||
expect(steps).toEqual(["willpatch", "patched"]);
|
expect(steps).toEqual(["willpatch", "patched"]);
|
||||||
@@ -818,9 +914,11 @@ describe("connected components and default values", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
|
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
|
||||||
|
|
||||||
await app.__updateProps({ initialRecipient: "James" }, true);
|
await app.__updateProps({ initialRecipient: "James" }, true);
|
||||||
|
await app.render();
|
||||||
expect(fixture.innerHTML).toBe("<div><div>Hello, James</div></div>");
|
expect(fixture.innerHTML).toBe("<div><div>Hello, James</div></div>");
|
||||||
|
|
||||||
await app.__updateProps({ initialRecipient: undefined }, true);
|
await app.__updateProps({ initialRecipient: undefined }, true);
|
||||||
|
await app.render();
|
||||||
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
|
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -829,11 +927,11 @@ describe("connected components and default values", () => {
|
|||||||
<templates>
|
<templates>
|
||||||
<div t-name="Message">
|
<div t-name="Message">
|
||||||
<t t-if="props.showId"><t t-esc="props.messageId"/></t>
|
<t t-if="props.showId"><t t-esc="props.messageId"/></t>
|
||||||
<t t-esc="props.message.content"/>
|
<t t-esc="storeProps.message.content"/>
|
||||||
</div>
|
</div>
|
||||||
<div t-name="Thread">
|
<div t-name="Thread">
|
||||||
<t t-if="props.showMessages">
|
<t t-if="props.showMessages">
|
||||||
<Message t-foreach="props.thread.messages" t-as="messageId" messageId="messageId" t-key="messageId"/>
|
<Message t-foreach="storeProps.thread.messages" t-as="messageId" messageId="messageId" t-key="messageId"/>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
<div t-name="App"><Thread threadId="props.threadId"/></div>
|
<div t-name="App"><Thread threadId="props.threadId"/></div>
|
||||||
@@ -887,13 +985,13 @@ describe("connected components and default values", () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const mutations = {
|
const actions = {
|
||||||
changeMessageContent({ state }, messageId, newContent) {
|
changeMessageContent({ state }, messageId, newContent) {
|
||||||
state.messages[messageId].content = newContent;
|
state.messages[messageId].content = newContent;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const store = new Store({ state, mutations });
|
const store = new Store({ state, actions });
|
||||||
(<any>env).store = store;
|
(<any>env).store = store;
|
||||||
const app = new App(env);
|
const app = new App(env);
|
||||||
|
|
||||||
@@ -903,10 +1001,96 @@ describe("connected components and default values", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await app.__updateProps({ threadId: 2 }, true);
|
await app.__updateProps({ threadId: 2 }, true);
|
||||||
|
await app.render();
|
||||||
expect(fixture.innerHTML).toBe("<div><div><div>200Message200</div></div></div>");
|
expect(fixture.innerHTML).toBe("<div><div><div>200Message200</div></div></div>");
|
||||||
|
|
||||||
store.commit("changeMessageContent", 200, "UpdatedMessage200");
|
store.dispatch("changeMessageContent", 200, "UpdatedMessage200");
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<div><div><div>200UpdatedMessage200</div></div></div>");
|
expect(fixture.innerHTML).toBe("<div><div><div>200UpdatedMessage200</div></div></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("connected child components stop listening to store when destroyed", async () => {
|
||||||
|
let steps: any = [];
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="Parent">
|
||||||
|
<Child t-if="state.child" />
|
||||||
|
</div>
|
||||||
|
<div t-name="Child"><t t-esc="storeProps.val"/></div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
class Child extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps(s) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component<any, any, any> {
|
||||||
|
components = { Child };
|
||||||
|
state = { 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 () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="App">
|
||||||
|
<t t-esc="storeProps.counter"/>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class App extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps = function(state) {
|
||||||
|
return {
|
||||||
|
counter: state.counter
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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>");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+56
-306
@@ -1,57 +1,33 @@
|
|||||||
import { Env } from "../../src/component/component";
|
import { Env } from "../../src/component/component";
|
||||||
import { Store } from "../../src/store/store";
|
import { Store, Getter } from "../../src/store/store";
|
||||||
import { nextMicroTick, nextTick } from "../helpers";
|
import { nextTick, nextMicroTick } from "../helpers";
|
||||||
|
|
||||||
|
|
||||||
describe("basic use", () => {
|
describe("basic use", () => {
|
||||||
test("commit a mutation", () => {
|
|
||||||
const state = { n: 1 };
|
|
||||||
const mutations = {
|
|
||||||
inc({ state }, delta) {
|
|
||||||
state.n += delta;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const store = new Store({ state, mutations });
|
|
||||||
|
|
||||||
expect(store.state.n).toBe(1);
|
|
||||||
store.commit("inc", 14);
|
|
||||||
expect(store.state.n).toBe(15);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("dispatch an action", () => {
|
test("dispatch an action", () => {
|
||||||
const state = { n: 1 };
|
const state = { n: 1 };
|
||||||
const mutations = {
|
const actions = {
|
||||||
inc({ state }, delta) {
|
inc({ state }, delta) {
|
||||||
state.n += delta;
|
state.n += delta;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const actions = {
|
const store = new Store({ state, actions });
|
||||||
inc({ commit }, delta) {
|
|
||||||
commit("inc", delta);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const store = new Store({ state, mutations, actions });
|
|
||||||
|
|
||||||
expect(store.state.n).toBe(1);
|
expect(store.state.n).toBe(1);
|
||||||
store.dispatch("inc", 14);
|
store.dispatch("inc", 14);
|
||||||
expect(store.state.n).toBe(15);
|
expect(store.state.n).toBe(15);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("dispatch an action + commit a mutation with positional arguments", () => {
|
test("dispatch an action with positional arguments", () => {
|
||||||
const state = { n1: 1, n2: 1, n3: 1 };
|
const state = { n1: 1, n2: 1, n3: 1 };
|
||||||
const mutations = {
|
|
||||||
|
const actions = {
|
||||||
batchInc({ state }, delta1, delta2, delta3) {
|
batchInc({ state }, delta1, delta2, delta3) {
|
||||||
state.n1 += delta1;
|
state.n1 += delta1;
|
||||||
state.n2 += delta2;
|
state.n2 += delta2;
|
||||||
state.n3 += delta3;
|
state.n3 += delta3;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const actions = {
|
const store = new Store({ state, actions });
|
||||||
batchInc({ commit }, delta1, delta2, delta3) {
|
|
||||||
commit("batchInc", delta1, delta2, delta3);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const store = new Store({ state, mutations, actions });
|
|
||||||
|
|
||||||
expect(store.state.n1).toBe(1);
|
expect(store.state.n1).toBe(1);
|
||||||
expect(store.state.n2).toBe(1);
|
expect(store.state.n2).toBe(1);
|
||||||
@@ -62,96 +38,58 @@ describe("basic use", () => {
|
|||||||
expect(store.state.n3).toBe(89);
|
expect(store.state.n3).toBe(89);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("modifying state outside of mutations trigger error", () => {
|
|
||||||
const state = { n: 1 };
|
|
||||||
const actions = {
|
|
||||||
inc({ state }) {
|
|
||||||
state.n++;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const store = new Store({ state, mutations: {}, actions });
|
|
||||||
|
|
||||||
expect(() => store.dispatch("inc")).toThrow();
|
|
||||||
expect(() => (store.state.n = 15)).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("can dispatch an action in an action", () => {
|
test("can dispatch an action in an action", () => {
|
||||||
const state = { n: 1 };
|
const state = { n: 1 };
|
||||||
const mutations = {
|
|
||||||
|
const actions = {
|
||||||
inc({ state }, delta) {
|
inc({ state }, delta) {
|
||||||
state.n += delta;
|
state.n += delta;
|
||||||
}
|
|
||||||
};
|
|
||||||
const actions = {
|
|
||||||
inc({ commit }, delta) {
|
|
||||||
commit("inc", delta);
|
|
||||||
},
|
},
|
||||||
inc100({ dispatch }) {
|
inc100({ dispatch }) {
|
||||||
dispatch("inc", 100);
|
dispatch("inc", 100);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({ state, mutations, actions });
|
const store = new Store({ state, actions });
|
||||||
|
|
||||||
expect(store.state.n).toBe(1);
|
expect(store.state.n).toBe(1);
|
||||||
store.dispatch("inc100");
|
store.dispatch("inc100");
|
||||||
expect(store.state.n).toBe(101);
|
expect(store.state.n).toBe(101);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can commit a mutation in a mutation", () => {
|
test("return data from dispatching an action", () => {
|
||||||
const state = { n: 1 };
|
const state = { n: 1 };
|
||||||
const mutations = {
|
const actions = {
|
||||||
inc({ state }) {
|
|
||||||
state.n++;
|
|
||||||
},
|
|
||||||
inc10({ commit }) {
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
commit("inc");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const store = new Store({ state, mutations });
|
|
||||||
|
|
||||||
expect(store.state.n).toBe(1);
|
|
||||||
store.commit("inc10");
|
|
||||||
expect(store.state.n).toBe(11);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("return data from committing a mutation", () => {
|
|
||||||
const state = { n: 1 };
|
|
||||||
const mutations = {
|
|
||||||
inc({ state }) {
|
inc({ state }) {
|
||||||
return ++state.n;
|
return ++state.n;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({ state, mutations });
|
const store = new Store({ state, actions });
|
||||||
|
|
||||||
expect(store.state.n).toBe(1);
|
expect(store.state.n).toBe(1);
|
||||||
const res = store.commit("inc");
|
const res = store.dispatch("inc");
|
||||||
expect(store.state.n).toBe(2);
|
expect(store.state.n).toBe(2);
|
||||||
expect(res).toBe(2);
|
expect(res).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("dispatch allow synchronizing between actions", async () => {
|
test("dispatch allow synchronizing between actions", async () => {
|
||||||
const state = { n: 1 };
|
const state = { n: 1 };
|
||||||
const mutations = {
|
const actions = {
|
||||||
inc({ state }, delta) {
|
inc({ state }, delta) {
|
||||||
state.n += delta;
|
state.n += delta;
|
||||||
},
|
},
|
||||||
setN({ state }, n) {
|
setN({ state }, n) {
|
||||||
state.n = n;
|
state.n = n;
|
||||||
}
|
|
||||||
};
|
|
||||||
const actions = {
|
|
||||||
async dosomething({ commit, dispatch }) {
|
|
||||||
await dispatch("setTo10");
|
|
||||||
commit("inc", 3);
|
|
||||||
},
|
},
|
||||||
async setTo10({ commit }) {
|
async dosomething({ dispatch }) {
|
||||||
|
await dispatch("setTo10");
|
||||||
|
dispatch("inc", 3);
|
||||||
|
},
|
||||||
|
async setTo10({ dispatch }) {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
commit("setN", 10);
|
dispatch("setN", 10);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({ state, mutations, actions });
|
const store = new Store({ state, actions });
|
||||||
|
|
||||||
expect(store.state.n).toBe(1);
|
expect(store.state.n).toBe(1);
|
||||||
store.dispatch("dosomething");
|
store.dispatch("dosomething");
|
||||||
@@ -162,26 +100,24 @@ describe("basic use", () => {
|
|||||||
|
|
||||||
test("return data from dispatching an action", async () => {
|
test("return data from dispatching an action", async () => {
|
||||||
const state = { n: 1 };
|
const state = { n: 1 };
|
||||||
const mutations = {
|
const actions = {
|
||||||
inc({ state }, delta) {
|
inc({ state }, delta) {
|
||||||
state.n += delta;
|
state.n += delta;
|
||||||
},
|
},
|
||||||
setN({ state }, n) {
|
setN({ state }, n) {
|
||||||
state.n = n;
|
state.n = n;
|
||||||
}
|
|
||||||
};
|
|
||||||
const actions = {
|
|
||||||
async dosomething({ commit, dispatch }) {
|
|
||||||
const val = await dispatch("setTo10");
|
|
||||||
commit("inc", val);
|
|
||||||
},
|
},
|
||||||
async setTo10({ commit }) {
|
async dosomething({ dispatch }) {
|
||||||
|
const val = await dispatch("setTo10");
|
||||||
|
dispatch("inc", val);
|
||||||
|
},
|
||||||
|
async setTo10({ dispatch }) {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
commit("setN", 10);
|
dispatch("setN", 10);
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({ state, mutations, actions });
|
const store = new Store({ state, actions });
|
||||||
|
|
||||||
expect(store.state.n).toBe(1);
|
expect(store.state.n).toBe(1);
|
||||||
store.dispatch("dosomething");
|
store.dispatch("dosomething");
|
||||||
@@ -219,7 +155,7 @@ describe("basic use", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const getters = {
|
const getters: { [key: string]: Getter } = {
|
||||||
beerTasterName({ state }, beerID) {
|
beerTasterName({ state }, beerID) {
|
||||||
return state.tasters[state.beers[beerID].tasterID].name;
|
return state.tasters[state.beers[beerID].tasterID].name;
|
||||||
},
|
},
|
||||||
@@ -227,161 +163,12 @@ describe("basic use", () => {
|
|||||||
return state.beers[1].name;
|
return state.beers[1].name;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({ state, mutations: {}, actions: {}, getters });
|
const store = new Store({ state, actions: {}, getters });
|
||||||
expect(store.getters).toBeDefined();
|
expect(store.getters).toBeDefined();
|
||||||
expect((<any>store.getters).bestBeerName()).toBe("bertinchamps");
|
expect((<any>store.getters).bestBeerName()).toBe("bertinchamps");
|
||||||
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
|
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getters are memoized", async () => {
|
|
||||||
const state = {
|
|
||||||
beers: {
|
|
||||||
1: {
|
|
||||||
id: 1,
|
|
||||||
name: "bertinchamps",
|
|
||||||
tasterID: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tasters: {
|
|
||||||
1: {
|
|
||||||
id: 1,
|
|
||||||
name: "aaron"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let n = 0;
|
|
||||||
const getters = {
|
|
||||||
beerTasterName({ state }, beerID) {
|
|
||||||
n++;
|
|
||||||
return state.tasters[state.beers[beerID].tasterID].name;
|
|
||||||
},
|
|
||||||
bestBeerName({ state }) {
|
|
||||||
n++;
|
|
||||||
return state.beers[1].name;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const store = new Store({ state, mutations: {}, actions: {}, getters });
|
|
||||||
expect((<any>store.getters).bestBeerName()).toBe("bertinchamps");
|
|
||||||
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
|
|
||||||
expect(n).toBe(2);
|
|
||||||
expect((<any>store.getters).bestBeerName()).toBe("bertinchamps");
|
|
||||||
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
|
|
||||||
expect(n).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getters taking Array as argument aren't memoized", async () => {
|
|
||||||
const state = {
|
|
||||||
beers: {
|
|
||||||
1: {
|
|
||||||
id: 1,
|
|
||||||
name: "bertinchamps",
|
|
||||||
tasterID: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let n = 0;
|
|
||||||
const getters = {
|
|
||||||
getBeerNames({ state }, beerIDs) {
|
|
||||||
n++;
|
|
||||||
return beerIDs.map(beerID => {
|
|
||||||
return state.beers[beerID].name;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const store = new Store({ state, mutations: {}, actions: {}, getters });
|
|
||||||
expect((<any>store.getters).getBeerNames([1])).toEqual(["bertinchamps"]);
|
|
||||||
expect(n).toBe(1);
|
|
||||||
expect((<any>store.getters).getBeerNames([1])).toEqual(["bertinchamps"]);
|
|
||||||
expect(n).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getters cache is nuked on store changes", async () => {
|
|
||||||
const state = {
|
|
||||||
beers: {
|
|
||||||
1: {
|
|
||||||
id: 1,
|
|
||||||
name: "bertinchamps",
|
|
||||||
tasterID: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tasters: {
|
|
||||||
1: {
|
|
||||||
id: 1,
|
|
||||||
name: "aaron"
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
id: 2,
|
|
||||||
name: "gery"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const mutations = {
|
|
||||||
changeTaster({ state }, { beerID, tasterID }) {
|
|
||||||
state.beers[beerID].tasterID = tasterID;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let n = 0;
|
|
||||||
const getters = {
|
|
||||||
beerTasterName({ state }, beerID) {
|
|
||||||
n++;
|
|
||||||
return state.tasters[state.beers[beerID].tasterID].name;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const store = new Store({
|
|
||||||
state,
|
|
||||||
mutations: mutations,
|
|
||||||
actions: {},
|
|
||||||
getters
|
|
||||||
});
|
|
||||||
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
|
|
||||||
expect(n).toBe(1);
|
|
||||||
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
|
|
||||||
expect(n).toBe(1);
|
|
||||||
|
|
||||||
store.commit("changeTaster", { beerID: 1, tasterID: 2 });
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect((<any>store.getters).beerTasterName(1)).toBe("gery");
|
|
||||||
expect(n).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getters cache is disabled during a mutation", async () => {
|
|
||||||
const state = {
|
|
||||||
beers: {
|
|
||||||
1: {
|
|
||||||
id: 1,
|
|
||||||
name: "bertinchamps"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const mutations = {
|
|
||||||
renameBeer({ state, getters }, beerID) {
|
|
||||||
expect(getters.beerName(beerID)).toBe("bertinchamps");
|
|
||||||
state.beers[1].name = "chouffe";
|
|
||||||
expect(getters.beerName(beerID)).toBe("chouffe");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let n = 0;
|
|
||||||
const getters = {
|
|
||||||
beerName({ state }, beerID) {
|
|
||||||
n++;
|
|
||||||
return state.beers[beerID].name;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const store = new Store({
|
|
||||||
state,
|
|
||||||
mutations: mutations,
|
|
||||||
actions: {},
|
|
||||||
getters
|
|
||||||
});
|
|
||||||
|
|
||||||
store.commit("renameBeer", 1);
|
|
||||||
expect((<any>store.getters).beerName(1)).toBe("chouffe");
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(n).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getters given to actions", async () => {
|
test("getters given to actions", async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
const state = {
|
const state = {
|
||||||
@@ -414,46 +201,10 @@ describe("basic use", () => {
|
|||||||
expect(getters.beerTasterName(1)).toBe("aaron");
|
expect(getters.beerTasterName(1)).toBe("aaron");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({ state, mutations: {}, actions, getters });
|
const store = new Store({ state, actions, getters });
|
||||||
store.dispatch("action");
|
store.dispatch("action");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getters given to mutations", async () => {
|
|
||||||
expect.assertions(3);
|
|
||||||
const state = {
|
|
||||||
beers: {
|
|
||||||
1: {
|
|
||||||
id: 1,
|
|
||||||
name: "bertinchamps",
|
|
||||||
tasterID: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tasters: {
|
|
||||||
1: {
|
|
||||||
id: 1,
|
|
||||||
name: "aaron"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const getters = {
|
|
||||||
beerTasterName({ state }, beerID) {
|
|
||||||
return state.tasters[state.beers[beerID].tasterID].name;
|
|
||||||
},
|
|
||||||
bestBeerName({ state }) {
|
|
||||||
return state.beers[1].name;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const mutations = {
|
|
||||||
mutation({ getters }) {
|
|
||||||
expect(getters).toBeDefined();
|
|
||||||
expect(getters.bestBeerName()).toBe("bertinchamps");
|
|
||||||
expect(getters.beerTasterName(1)).toBe("aaron");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const store = new Store({ state, mutations, actions: {}, getters });
|
|
||||||
store.commit("mutation");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("can use getters inside a getter", () => {
|
test("can use getters inside a getter", () => {
|
||||||
const getters = {
|
const getters = {
|
||||||
a({ getters }) {
|
a({ getters }) {
|
||||||
@@ -466,7 +217,7 @@ describe("basic use", () => {
|
|||||||
return `c${i}`;
|
return `c${i}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({ getters });
|
const store = new Store({ getters, state: {}, actions: {} });
|
||||||
|
|
||||||
expect(store.getters.a()).toBe("bc1");
|
expect(store.getters.a()).toBe("bc1");
|
||||||
});
|
});
|
||||||
@@ -474,54 +225,54 @@ describe("basic use", () => {
|
|||||||
|
|
||||||
describe("advanced state properties", () => {
|
describe("advanced state properties", () => {
|
||||||
test("state in the store is reference equal after mutation", async () => {
|
test("state in the store is reference equal after mutation", async () => {
|
||||||
const mutations = {
|
const actions = {
|
||||||
donothing() {}
|
donothing() {}
|
||||||
};
|
};
|
||||||
const store = new Store({ state:{}, mutations });
|
const store = new Store({ state: {}, actions });
|
||||||
const state = store.state;
|
const state = store.state;
|
||||||
store.commit("donothing");
|
store.dispatch("donothing");
|
||||||
expect(store.state).toBe(state);
|
expect(store.state).toBe(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can use array properties in mutations", () => {
|
test("can use array properties in mutations", () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
const state = { a: [1, 2, 3] };
|
const state = { a: [1, 2, 3] };
|
||||||
const mutations = {
|
const actions = {
|
||||||
m({ state }) {
|
m({ state }) {
|
||||||
expect(state.a.length).toBe(3);
|
expect(state.a.length).toBe(3);
|
||||||
const l = state.a.push(53);
|
const l = state.a.push(53);
|
||||||
expect(l).toBe(4);
|
expect(l).toBe(4);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({ state, mutations });
|
const store = new Store({ state, actions });
|
||||||
store.commit("m");
|
store.dispatch("m");
|
||||||
expect(store.state.a).toEqual([1, 2, 3, 53]);
|
expect(store.state.a).toEqual([1, 2, 3, 53]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can use object assign in store", async () => {
|
test("can use object assign in store", async () => {
|
||||||
const mutations = {
|
const actions = {
|
||||||
dosomething({ state }) {
|
dosomething({ state }) {
|
||||||
Object.assign(state.westmalle, { a: 3, b: 4 });
|
Object.assign(state.westmalle, { a: 3, b: 4 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({
|
const store = new Store({
|
||||||
state: { westmalle: { a: 1, b: 2 } },
|
state: { westmalle: { a: 1, b: 2 } },
|
||||||
mutations
|
actions
|
||||||
});
|
});
|
||||||
store.commit("dosomething");
|
store.dispatch("dosomething");
|
||||||
expect(store.state.westmalle).toEqual({ a: 3, b: 4 });
|
expect(store.state.westmalle).toEqual({ a: 3, b: 4 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("aku reactive store state 1", async () => {
|
test("aku reactive store state 1", async () => {
|
||||||
const mutations = {
|
const actions = {
|
||||||
inc({ state }) {
|
inc({ state }) {
|
||||||
state.counter++;
|
state.counter++;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const state = { counter: 0 };
|
const state = { counter: 0 };
|
||||||
const store = new Store({ state, mutations });
|
const store = new Store({ state, actions });
|
||||||
expect(store.state.counter).toBe(0);
|
expect(store.state.counter).toBe(0);
|
||||||
store.commit("inc", {});
|
store.dispatch("inc", {});
|
||||||
expect(store.state.counter).toBe(1);
|
expect(store.state.counter).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -530,17 +281,17 @@ describe("updates triggered by the store", () => {
|
|||||||
test("multiple commits trigger one update", async () => {
|
test("multiple commits trigger one update", async () => {
|
||||||
let updateCounter = 0;
|
let updateCounter = 0;
|
||||||
const state = { n: 1 };
|
const state = { n: 1 };
|
||||||
const mutations = {
|
const actions = {
|
||||||
inc({ state }, delta) {
|
inc({ state }, delta) {
|
||||||
state.n += delta;
|
state.n += delta;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({ state, mutations });
|
const store = new Store({ state, actions });
|
||||||
store.on("update", null, () => updateCounter++);
|
store.on("update", null, () => updateCounter++);
|
||||||
|
|
||||||
store.commit("inc", 14);
|
store.dispatch("inc", 14);
|
||||||
expect(updateCounter).toBe(0);
|
expect(updateCounter).toBe(0);
|
||||||
store.commit("inc", 50);
|
store.dispatch("inc", 50);
|
||||||
expect(updateCounter).toBe(0);
|
expect(updateCounter).toBe(0);
|
||||||
await nextMicroTick();
|
await nextMicroTick();
|
||||||
expect(updateCounter).toBe(1);
|
expect(updateCounter).toBe(1);
|
||||||
@@ -549,7 +300,7 @@ describe("updates triggered by the store", () => {
|
|||||||
test("empty commits do not trigger updates", async () => {
|
test("empty commits do not trigger updates", async () => {
|
||||||
let updateCounter = 0;
|
let updateCounter = 0;
|
||||||
const state = { n: 1 };
|
const state = { n: 1 };
|
||||||
const mutations = {
|
const actions = {
|
||||||
inc({ state }, delta) {
|
inc({ state }, delta) {
|
||||||
state.n += delta;
|
state.n += delta;
|
||||||
},
|
},
|
||||||
@@ -559,20 +310,19 @@ describe("updates triggered by the store", () => {
|
|||||||
state.n = val;
|
state.n = val;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({ state, mutations });
|
const store = new Store({ state, actions });
|
||||||
store.on("update", null, () => updateCounter++);
|
store.on("update", null, () => updateCounter++);
|
||||||
|
|
||||||
store.commit("noop");
|
store.dispatch("noop");
|
||||||
await nextMicroTick();
|
await nextMicroTick();
|
||||||
expect(updateCounter).toBe(0);
|
expect(updateCounter).toBe(0);
|
||||||
|
|
||||||
store.commit("inc", 50);
|
store.dispatch("inc", 50);
|
||||||
await nextMicroTick();
|
await nextMicroTick();
|
||||||
expect(updateCounter).toBe(1);
|
expect(updateCounter).toBe(1);
|
||||||
|
|
||||||
store.commit("noop2");
|
store.dispatch("noop2");
|
||||||
await nextMicroTick();
|
await nextMicroTick();
|
||||||
expect(updateCounter).toBe(1);
|
expect(updateCounter).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class App extends owl.Component {
|
|||||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||||
const msg = Object.assign({}, messages[i]);
|
const msg = Object.assign({}, messages[i]);
|
||||||
msg.author += "!!!";
|
msg.author += "!!!";
|
||||||
this.set(messages, i, msg);
|
owl.Observer.set(messages, i, msg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Likes Counter Widget
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class Counter extends owl.Component {
|
||||||
|
state = { counter: 0 };
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.counter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Message Widget
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class Message extends owl.Component {
|
||||||
|
components = { Counter };
|
||||||
|
|
||||||
|
shouldUpdate(nextProps) {
|
||||||
|
return nextProps.message !== this.props.message;
|
||||||
|
}
|
||||||
|
removeMessage() {
|
||||||
|
this.trigger("remove-message", {
|
||||||
|
id: this.props.message.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Root Widget
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class App extends owl.Component {
|
||||||
|
components = { Message };
|
||||||
|
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
benchmark(message, fn, callback) {
|
||||||
|
if (this.state.multipleFlag) {
|
||||||
|
const N = 20;
|
||||||
|
let n = N;
|
||||||
|
let total = 0;
|
||||||
|
let cb = info => {
|
||||||
|
let finalize = () => {
|
||||||
|
n--;
|
||||||
|
total += info.delta;
|
||||||
|
if (n === 0) {
|
||||||
|
const avg = total / N;
|
||||||
|
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._benchmark(message, fn, cb);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.state.clearAfterFlag) {
|
||||||
|
this._benchmark(
|
||||||
|
"clear",
|
||||||
|
() => {
|
||||||
|
this.state.messages = [];
|
||||||
|
},
|
||||||
|
finalize,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
finalize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._benchmark(message, fn, cb);
|
||||||
|
} else {
|
||||||
|
this._benchmark(message, fn, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_benchmark(message, fn, cb, log = true) {
|
||||||
|
setTimeout(() => {
|
||||||
|
startMeasure(message);
|
||||||
|
fn();
|
||||||
|
stopMeasure(info => {
|
||||||
|
if (log) {
|
||||||
|
this.log(info.msg);
|
||||||
|
}
|
||||||
|
if (cb) {
|
||||||
|
cb(info);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessages(n) {
|
||||||
|
this.benchmark("add " + n, () => {
|
||||||
|
const newMessages = buildData(n);
|
||||||
|
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._benchmark("clear", () => {
|
||||||
|
this.state.messages = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSomeMessages() {
|
||||||
|
this.benchmark("update every 10th", () => {
|
||||||
|
const messages = this.state.messages;
|
||||||
|
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||||
|
const msg = Object.assign({}, messages[i]);
|
||||||
|
msg.author += "!!!";
|
||||||
|
messages[i] = msg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMessage(event) {
|
||||||
|
this.benchmark("remove message", () => {
|
||||||
|
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
|
||||||
|
this.state.messages.splice(index, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
log(str, isBold) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
if (isBold) {
|
||||||
|
div.classList.add("bold");
|
||||||
|
}
|
||||||
|
div.textContent = `> ${str}`;
|
||||||
|
this.refs.log.appendChild(div);
|
||||||
|
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLog() {
|
||||||
|
this.refs.log.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMultiple() {
|
||||||
|
this.state.multipleFlag = !this.state.multipleFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleClear() {
|
||||||
|
this.state.clearAfterFlag = !this.state.clearAfterFlag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Application initialization
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
async function start() {
|
||||||
|
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||||
|
const env = {
|
||||||
|
qweb: new owl.QWeb(templates)
|
||||||
|
};
|
||||||
|
const app = new App(env);
|
||||||
|
app.mount(document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
start();
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>OWL 0.18.0 Benchmark</title>
|
||||||
|
<link href="../shared/main.css" rel="stylesheet"/>
|
||||||
|
<script src='owl.js'></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id='main'></div>
|
||||||
|
<script src='app.js' type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
|||||||
|
<templates>
|
||||||
|
<div t-name="App" class="main">
|
||||||
|
<div class="left-thing">
|
||||||
|
<div class="title">Actions</div>
|
||||||
|
<div class="panel">
|
||||||
|
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||||
|
<button t-on-click="addMessages(1000)">Add 1k messages</button>
|
||||||
|
<button t-on-click="addMessages(10000)">Add 10k messages</button>
|
||||||
|
<button t-on-click="addMessages(30000)">Add 30k messages</button>
|
||||||
|
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
|
||||||
|
<button t-on-click="clear">Clear</button>
|
||||||
|
</div>
|
||||||
|
<div class="flags">
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
|
||||||
|
<label for="multipleflag">Do it 20x</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
|
||||||
|
<label for="clearFlag">Clear after</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
|
||||||
|
<hr/>
|
||||||
|
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
|
||||||
|
<div class="log">
|
||||||
|
<div class="log-content" t-ref="log"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-thing">
|
||||||
|
<div class="content" t-on-remove-message="removeMessage">
|
||||||
|
<t t-foreach="state.messages" t-as="message">
|
||||||
|
<Message t-key="message.id" message="message"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Message" class="message">
|
||||||
|
<span class="author"><t t-esc="props.message.author"/></span>
|
||||||
|
<span class="msg"><t t-esc="props.message.msg"/></span>
|
||||||
|
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||||
|
<Counter/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Counter">
|
||||||
|
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
<li><a href="benchmarks/owl-0.15.0">OWL 0.15.0</a></li>
|
<li><a href="benchmarks/owl-0.15.0">OWL 0.15.0</a></li>
|
||||||
<li><a href="benchmarks/owl-0.16.0">OWL 0.16.0</a></li>
|
<li><a href="benchmarks/owl-0.16.0">OWL 0.16.0</a></li>
|
||||||
<li><a href="benchmarks/owl-0.17.0">OWL 0.17.0</a></li>
|
<li><a href="benchmarks/owl-0.17.0">OWL 0.17.0</a></li>
|
||||||
|
<li><a href="benchmarks/owl-0.18.0">OWL 0.18.0</a></li>
|
||||||
<li><a href="benchmarks/owl-master">OWL Master</a></li>
|
<li><a href="benchmarks/owl-master">OWL Master</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
+293
-74
@@ -268,56 +268,42 @@ const TODO_APP_STORE = `// This example is an implementation of the TodoList app
|
|||||||
|
|
||||||
const ENTER_KEY = 13;
|
const ENTER_KEY = 13;
|
||||||
const ESC_KEY = 27;
|
const ESC_KEY = 27;
|
||||||
const LOCALSTORAGE_KEY = "todos-odoo";
|
const LOCALSTORAGE_KEY = "todomvc";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Store Definition
|
// Store Definition
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
const actions = {
|
const actions = {
|
||||||
addTodo({ commit }, title) {
|
|
||||||
commit("addTodo", title);
|
|
||||||
},
|
|
||||||
removeTodo({ commit }, id) {
|
|
||||||
commit("removeTodo", id);
|
|
||||||
},
|
|
||||||
toggleTodo({ state, commit }, id) {
|
|
||||||
const todo = state.todos.find(t => t.id === id);
|
|
||||||
commit("editTodo", { id, completed: !todo.completed });
|
|
||||||
},
|
|
||||||
clearCompleted({ state, commit }) {
|
|
||||||
state.todos
|
|
||||||
.filter(todo => todo.completed)
|
|
||||||
.forEach(todo => {
|
|
||||||
commit("removeTodo", todo.id);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
toggleAll({ state, commit }, completed) {
|
|
||||||
state.todos.forEach(todo => {
|
|
||||||
commit("editTodo", {
|
|
||||||
id: todo.id,
|
|
||||||
completed
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
editTodo({ commit }, { id, title }) {
|
|
||||||
commit("editTodo", { id, title });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mutations = {
|
|
||||||
addTodo({ state }, title) {
|
addTodo({ state }, title) {
|
||||||
const id = state.nextId++;
|
state.todos.push({
|
||||||
const todo = {
|
id: state.nextId++,
|
||||||
id,
|
|
||||||
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) {
|
||||||
|
const todo = state.todos.find(t => t.id === id);
|
||||||
|
dispatch("editTodo", { id, completed: !todo.completed });
|
||||||
|
},
|
||||||
|
clearCompleted({ state, dispatch }) {
|
||||||
|
state.todos
|
||||||
|
.filter(todo => todo.completed)
|
||||||
|
.forEach(todo => {
|
||||||
|
dispatch("removeTodo", todo.id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
toggleAll({ state, dispatch }, completed) {
|
||||||
|
state.todos.forEach(todo => {
|
||||||
|
dispatch("editTodo", {
|
||||||
|
id: todo.id,
|
||||||
|
completed
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
editTodo({ state }, { id, title, completed }) {
|
editTodo({ state }, { id, title, completed }) {
|
||||||
const todo = state.todos.find(t => t.id === id);
|
const todo = state.todos.find(t => t.id === id);
|
||||||
if (title !== undefined) {
|
if (title !== undefined) {
|
||||||
@@ -329,24 +315,20 @@ const mutations = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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() {
|
function makeStore() {
|
||||||
const todos = JSON.parse(
|
const state = loadState();
|
||||||
window.localStorage.getItem(LOCALSTORAGE_KEY) || "[]"
|
const store = new owl.store.Store({ state, actions });
|
||||||
);
|
store.on("update", null, () => saveState(store.state));
|
||||||
const nextId = Math.max(0, ...todos.map(t => t.id || 0)) + 1;
|
|
||||||
const state = {
|
|
||||||
todos,
|
|
||||||
nextId
|
|
||||||
};
|
|
||||||
const store = new owl.store.Store({
|
|
||||||
state,
|
|
||||||
actions,
|
|
||||||
mutations
|
|
||||||
});
|
|
||||||
store.on("update", null, () => {
|
|
||||||
const state = JSON.stringify(store.state.todos);
|
|
||||||
window.localStorage.setItem(LOCALSTORAGE_KEY, state);
|
|
||||||
});
|
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,11 +339,11 @@ class TodoItem extends owl.Component {
|
|||||||
state = { isEditing: false };
|
state = { isEditing: false };
|
||||||
|
|
||||||
removeTodo() {
|
removeTodo() {
|
||||||
this.env.dispatch("removeTodo", this.props.id);
|
this.env.store.dispatch("removeTodo", this.props.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTodo() {
|
toggleTodo() {
|
||||||
this.env.dispatch("toggleTodo", this.props.id);
|
this.env.store.dispatch("toggleTodo", this.props.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async editTodo() {
|
async editTodo() {
|
||||||
@@ -393,7 +375,7 @@ class TodoItem extends owl.Component {
|
|||||||
if (!value) {
|
if (!value) {
|
||||||
this.removeTodo(this.props.id);
|
this.removeTodo(this.props.id);
|
||||||
} else {
|
} else {
|
||||||
this.env.dispatch("editTodo", {
|
this.env.store.dispatch("editTodo", {
|
||||||
id: this.props.id,
|
id: this.props.id,
|
||||||
title: value
|
title: value
|
||||||
});
|
});
|
||||||
@@ -415,7 +397,7 @@ class TodoApp extends owl.store.ConnectedComponent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
get visibleTodos() {
|
get visibleTodos() {
|
||||||
let todos = this.props.todos;
|
let todos = this.storeProps.todos;
|
||||||
if (this.state.filter === "active") {
|
if (this.state.filter === "active") {
|
||||||
todos = todos.filter(t => !t.completed);
|
todos = todos.filter(t => !t.completed);
|
||||||
}
|
}
|
||||||
@@ -426,11 +408,11 @@ class TodoApp extends owl.store.ConnectedComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get allChecked() {
|
get allChecked() {
|
||||||
return this.props.todos.every(todo => todo.completed);
|
return this.storeProps.todos.every(todo => todo.completed);
|
||||||
}
|
}
|
||||||
|
|
||||||
get remaining() {
|
get remaining() {
|
||||||
return this.props.todos.filter(todo => !todo.completed).length;
|
return this.storeProps.todos.filter(todo => !todo.completed).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
get remainingText() {
|
get remainingText() {
|
||||||
@@ -442,20 +424,12 @@ class TodoApp extends owl.store.ConnectedComponent {
|
|||||||
if (ev.keyCode === ENTER_KEY) {
|
if (ev.keyCode === ENTER_KEY) {
|
||||||
const title = ev.target.value;
|
const title = ev.target.value;
|
||||||
if (title.trim()) {
|
if (title.trim()) {
|
||||||
this.env.dispatch("addTodo", title);
|
this.dispatch("addTodo", title);
|
||||||
}
|
}
|
||||||
ev.target.value = "";
|
ev.target.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCompleted() {
|
|
||||||
this.env.dispatch("clearCompleted");
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleAll() {
|
|
||||||
this.env.dispatch("toggleAll", !this.allChecked);
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilter(filter) {
|
setFilter(filter) {
|
||||||
this.state.filter = filter;
|
this.state.filter = filter;
|
||||||
}
|
}
|
||||||
@@ -469,7 +443,6 @@ const qweb = new owl.QWeb(TEMPLATES);
|
|||||||
const env = {
|
const env = {
|
||||||
qweb,
|
qweb,
|
||||||
store,
|
store,
|
||||||
dispatch: store.dispatch.bind(store),
|
|
||||||
};
|
};
|
||||||
const app = new TodoApp(env);
|
const app = new TodoApp(env);
|
||||||
app.mount(document.body);
|
app.mount(document.body);
|
||||||
@@ -481,8 +454,8 @@ 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="props.todos.length">
|
<section class="main" t-if="storeProps.todos.length">
|
||||||
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="toggleAll"/>
|
<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">
|
||||||
<t t-foreach="visibleTodos" t-as="todo">
|
<t t-foreach="visibleTodos" t-as="todo">
|
||||||
@@ -490,7 +463,7 @@ const TODO_APP_STORE_XML = `<templates>
|
|||||||
</t>
|
</t>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<footer class="footer" t-if="props.todos.length">
|
<footer class="footer" t-if="storeProps.todos.length">
|
||||||
<span class="todo-count">
|
<span class="todo-count">
|
||||||
<strong>
|
<strong>
|
||||||
<t t-esc="remaining"/>
|
<t t-esc="remaining"/>
|
||||||
@@ -508,7 +481,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="props.todos.length gt remaining" t-on-click="clearCompleted">
|
<button class="clear-completed" t-if="storeProps.todos.length gt remaining" t-on-click="dispatch('clearCompleted')">
|
||||||
Clear completed
|
Clear completed
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -1334,6 +1307,246 @@ const FORM_XML = `<templates>
|
|||||||
</templates>
|
</templates>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const WMS = `// This example is slightly more complex than usual. We demonstrate
|
||||||
|
// here a way to manage sub windows in Owl, declaratively. This is still just a
|
||||||
|
// demonstration. Managing windows can be as complex as we want. For example,
|
||||||
|
// we could implement the following features:
|
||||||
|
// - resizing windows
|
||||||
|
// - minimizing windows
|
||||||
|
// - configuration options for windows to make a window non resizeable
|
||||||
|
// - minimal width/height
|
||||||
|
// - better heuristic for initial window position
|
||||||
|
// - ...
|
||||||
|
|
||||||
|
class HelloWorld extends owl.Component {}
|
||||||
|
|
||||||
|
class Counter extends owl.Component {
|
||||||
|
state = { value: 0 };
|
||||||
|
|
||||||
|
inc() {
|
||||||
|
this.state.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Window extends owl.Component {
|
||||||
|
get style() {
|
||||||
|
let { width, height, top, left, zindex } = this.props.info;
|
||||||
|
|
||||||
|
return \`width: \${width}px;height: \${height}px;top:\${top}px;left:\${left}px;z-index:\${zindex}\`;
|
||||||
|
}
|
||||||
|
close() {
|
||||||
|
this.trigger("close-window", { id: this.props.info.id });
|
||||||
|
}
|
||||||
|
startDragAndDrop(ev) {
|
||||||
|
this.updateZIndex();
|
||||||
|
this.el.classList.add('dragging');
|
||||||
|
const offsetX = this.props.info.left - ev.pageX;
|
||||||
|
const offsetY = this.props.info.top - ev.pageY;
|
||||||
|
let left, top;
|
||||||
|
|
||||||
|
const el = this.el;
|
||||||
|
const self = this;
|
||||||
|
window.addEventListener("mousemove", moveWindow);
|
||||||
|
window.addEventListener("mouseup", stopDnD, { once: true });
|
||||||
|
|
||||||
|
function moveWindow(ev) {
|
||||||
|
left = Math.max(offsetX + ev.pageX, 0);
|
||||||
|
top = Math.max(offsetY + ev.pageY, 0);
|
||||||
|
el.style.left = \`\${left}px\`;
|
||||||
|
el.style.top = \`\${top}px\`;
|
||||||
|
}
|
||||||
|
function stopDnD() {
|
||||||
|
window.removeEventListener("mousemove", moveWindow);
|
||||||
|
const options = { id: self.props.info.id, left, top };
|
||||||
|
self.el.classList.remove('dragging');
|
||||||
|
self.trigger("set-window-position", options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateZIndex() {
|
||||||
|
this.trigger("update-z-index", { id: this.props.info.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WindowManager extends owl.Component {
|
||||||
|
components = { Window };
|
||||||
|
windows = [];
|
||||||
|
nextId = 1;
|
||||||
|
currentZindex = 1;
|
||||||
|
addWindow(name) {
|
||||||
|
const info = this.env.windows.find(w => w.name === name);
|
||||||
|
const id = \`w\${this.nextId++}\`;
|
||||||
|
this.windows.push({
|
||||||
|
id: id,
|
||||||
|
title: info.title,
|
||||||
|
width: info.defaultWidth,
|
||||||
|
height: info.defaultHeight,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
zindex: this.currentZindex++
|
||||||
|
});
|
||||||
|
this.components[id] = info.component;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
closeWindow(ev) {
|
||||||
|
const id = ev.detail.id;
|
||||||
|
delete this.components[id];
|
||||||
|
const index = this.windows.findIndex(w => w.id === id);
|
||||||
|
this.windows.splice(index, 1);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
setWindowPosition(ev) {
|
||||||
|
const id = ev.detail.id;
|
||||||
|
const w = this.windows.find(w => w.id === id);
|
||||||
|
w.top = ev.detail.top;
|
||||||
|
w.left = ev.detail.left;
|
||||||
|
}
|
||||||
|
updateZIndex(ev) {
|
||||||
|
const id = ev.detail.id;
|
||||||
|
const w = this.windows.find(w => w.id === id);
|
||||||
|
w.zindex = this.currentZindex++;
|
||||||
|
ev.target.style["z-index"] = w.zindex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class App extends owl.Component {
|
||||||
|
components = { WindowManager };
|
||||||
|
|
||||||
|
addWindow(name) {
|
||||||
|
this.refs.wm.addWindow(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const qweb = new owl.QWeb(TEMPLATES);
|
||||||
|
const windows = [
|
||||||
|
{
|
||||||
|
name: "Hello",
|
||||||
|
title: "Hello",
|
||||||
|
component: HelloWorld,
|
||||||
|
defaultWidth: 200,
|
||||||
|
defaultHeight: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Counter",
|
||||||
|
title: "Click Counter",
|
||||||
|
component: Counter,
|
||||||
|
defaultWidth: 300,
|
||||||
|
defaultHeight: 120
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const env = { qweb, windows };
|
||||||
|
const app = new App(env);
|
||||||
|
app.mount(document.body);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const WMS_XML = `<templates>
|
||||||
|
<div t-name="Window" class="window" t-att-style="style" t-on-click="updateZIndex">
|
||||||
|
<div class="header">
|
||||||
|
<span t-on-mousedown="startDragAndDrop"><t t-esc="props.info.title"/></span>
|
||||||
|
<span class="close" t-on-click="close">×</span>
|
||||||
|
</div>
|
||||||
|
<t t-slot="default"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="WindowManager" class="window-manager"
|
||||||
|
t-on-close-window="closeWindow"
|
||||||
|
t-on-update-z-index="updateZIndex"
|
||||||
|
t-on-set-window-position="setWindowPosition">
|
||||||
|
<Window t-foreach="windows" t-as="w" t-key="w.id" info="w">
|
||||||
|
<t t-component="{{w.id}}"/>
|
||||||
|
</Window>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="App" class="app">
|
||||||
|
<WindowManager t-ref="wm"/>
|
||||||
|
<div class="menubar">
|
||||||
|
<button t-on-click="addWindow('Hello')">Say Hello</button>
|
||||||
|
<button t-on-click="addWindow('Counter')">Counter</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="HelloWorld">
|
||||||
|
World
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Counter" class="counter">
|
||||||
|
<button t-on-click="inc">Inc</button>
|
||||||
|
<span><t t-esc="state.value"/></span>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const WMS_CSS = `body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-manager {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menubar {
|
||||||
|
background-color: #875a7b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menubar button {
|
||||||
|
height: 40px;
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 30px auto;
|
||||||
|
border: 1px solid gray;
|
||||||
|
background-color: white;
|
||||||
|
position: absolute;
|
||||||
|
box-shadow: 1px 1px 2px 1px grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window.dragging {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window .header {
|
||||||
|
background-color: #875a7b;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 24px;
|
||||||
|
color: white;
|
||||||
|
line-height: 30px;
|
||||||
|
padding-left: 5px;
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window .header .close {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 22px;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.counter button {
|
||||||
|
width: 80px;
|
||||||
|
height:40px;
|
||||||
|
font-size: 20px;
|
||||||
|
}`;
|
||||||
|
|
||||||
export const SAMPLES = [
|
export const SAMPLES = [
|
||||||
{
|
{
|
||||||
description: "Components",
|
description: "Components",
|
||||||
@@ -1376,6 +1589,12 @@ export const SAMPLES = [
|
|||||||
xml: SLOTS_XML,
|
xml: SLOTS_XML,
|
||||||
css: SLOTS_CSS
|
css: SLOTS_CSS
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Window Management System",
|
||||||
|
code: WMS,
|
||||||
|
xml: WMS_XML,
|
||||||
|
css: WMS_CSS,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "Asynchronous components",
|
description: "Asynchronous components",
|
||||||
code: ASYNC_COMPONENTS,
|
code: ASYNC_COMPONENTS,
|
||||||
|
|||||||
Reference in New Issue
Block a user