Compare commits

..

24 Commits

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

Same applies for t-raw.

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