mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14ceb382ef | |||
| 9846b2e997 | |||
| cfc2fb2ba2 | |||
| d57017b6ed | |||
| 5bf7a5c671 | |||
| 9b589af75a | |||
| 471fd49063 | |||
| 4fc3423682 | |||
| ffb3263c79 | |||
| e1acf66143 | |||
| 2a40907e35 | |||
| 1c0dd79955 | |||
| 9fad164c79 | |||
| 53a8841914 | |||
| 19f0863aa3 | |||
| 28f7b44f9e | |||
| fe46a6ccee | |||
| 3c23a7b246 | |||
| c52799de2c | |||
| 4edb90f44b | |||
| 0c1a7aabae | |||
| cfdce29ce7 | |||
| b344d73e07 | |||
| 4cd08d25c8 | |||
| f9861040c8 | |||
| 40d8090232 | |||
| 55be6437f1 | |||
| 6ff4dd74bf | |||
| 3628b49c37 |
@@ -14,6 +14,45 @@ related projects. OWL's main features are:
|
|||||||
|
|
||||||
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the OWL framework.
|
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the OWL framework.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Here is a short example to illustrate interactive components:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { Component, QWeb } from 'owl'
|
||||||
|
import { xml } from 'owl/tags'
|
||||||
|
|
||||||
|
class Counter extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<button t-on-click="increment">
|
||||||
|
Click Me! [<t t-esc="state.value"/>]
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
state = { value: 0 };
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class App extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<span>Hello Owl</span>
|
||||||
|
<Counter />
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
static components = { Counter };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App({ qweb: new QWeb() });
|
||||||
|
app.mount(document.body);
|
||||||
|
```
|
||||||
|
|
||||||
|
More interesting examples can be found on the
|
||||||
|
[playground](https://odoo.github.io/owl/playground) application.
|
||||||
|
|
||||||
## OWL's Design Principles
|
## OWL's Design Principles
|
||||||
|
|
||||||
OWL is designed to be used in highly dynamic applications where changing
|
OWL is designed to be used in highly dynamic applications where changing
|
||||||
@@ -31,48 +70,29 @@ requirements are common and code needs to be maintained by large teams.
|
|||||||
it is not ok to use standard web tools based on `npm`.
|
it is not ok to use standard web tools based on `npm`.
|
||||||
|
|
||||||
Owl is not designed to be fast nor small (even though it is quite good on those
|
Owl is not designed to be fast nor small (even though it is quite good on those
|
||||||
two topics). It is a no nonsense framework to build applications. There is only
|
two topics). It is a no nonsense framework to build applications. There is only
|
||||||
one way to define components (with classes).
|
one way to define components (with classes).
|
||||||
|
|
||||||
If you are interested in a comparison with React or Vue, you will
|
If you are interested in a comparison with React or Vue, you will
|
||||||
find some more information [here](doc/comparison.md).
|
find some more information [here](doc/comparison.md).
|
||||||
|
|
||||||
## Example
|
## Documentation
|
||||||
|
|
||||||
Here is a short example to illustrate interactive components:
|
The complete documentation can be found [here](doc/readme.md). The most important sections are:
|
||||||
|
|
||||||
```xml
|
- [Quick Start](doc/quick_start.md)
|
||||||
<templates>
|
- [Component](doc/component.md)
|
||||||
<button t-name="Counter" t-on-click="increment">
|
- [QWeb](doc/qweb.md)
|
||||||
Click Me! [<t t-esc="state.value"/>]
|
|
||||||
</button>
|
|
||||||
</templates>
|
|
||||||
```
|
|
||||||
|
|
||||||
```javascript
|
Found an issue in the documentation? A broken link? Some outdated information?
|
||||||
class Counter extends owl.Component {
|
Submit a PR!
|
||||||
state = { value: 0 };
|
|
||||||
|
|
||||||
increment() {
|
|
||||||
this.state.value++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const qweb = new owl.QWeb(TEMPLATES);
|
|
||||||
const counter = new Counter({ qweb });
|
|
||||||
counter.mount(document.body);
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that we assume here that the xml templates are available in the `TEMPLATES`
|
|
||||||
string. More interesting examples can be found on the
|
|
||||||
[playground](https://odoo.github.io/owl/playground) application.
|
|
||||||
|
|
||||||
## Installing/Building
|
## Installing/Building
|
||||||
|
|
||||||
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.19.0.js](https://github.com/odoo/owl/releases/download/v0.19.0/owl.js)
|
- [owl-0.21.0.js](https://github.com/odoo/owl/releases/download/v0.21.0/owl.js)
|
||||||
- [owl-0.19.0.min.js](https://github.com/odoo/owl/releases/download/v0.19.0/owl.min.js)
|
- [owl-0.21.0.min.js](https://github.com/odoo/owl/releases/download/v0.21.0/owl.min.js)
|
||||||
|
|
||||||
Some npm scripts are available:
|
Some npm scripts are available:
|
||||||
|
|
||||||
@@ -86,17 +106,6 @@ Some npm scripts are available:
|
|||||||
| `npm run tools` | build tools applications, start a static server (see [here](doc/tooling.md)) |
|
| `npm run tools` | build tools applications, start a static server (see [here](doc/tooling.md)) |
|
||||||
| `npm run tools:watch` | same as `tools`, but with a watcher to rebuild owl |
|
| `npm run tools:watch` | same as `tools`, but with a watcher to rebuild owl |
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
The complete documentation can be found [here](doc/readme.md). The most important sections are:
|
|
||||||
|
|
||||||
- [Quick Start](doc/quick_start.md)
|
|
||||||
- [Component](doc/component.md)
|
|
||||||
- [QWeb](doc/qweb.md)
|
|
||||||
|
|
||||||
Found an issue in the documentation? A broken link? Some outdated information?
|
|
||||||
Submit a PR!
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
OWL is [GPL licensed](./LICENSE).
|
OWL is [LGPL licensed](./LICENSE).
|
||||||
|
|||||||
+4
-4
@@ -31,11 +31,11 @@ than React and Vue. Also, jQuery is not the same kind of framework, but it is in
|
|||||||
|
|
||||||
## Class Based
|
## Class Based
|
||||||
|
|
||||||
Both React and Vue moved away from defining components with classes. They prefer
|
Both React and Vue moved away from defining components with classes. They prefer
|
||||||
a more functional approach, in particular, with the new `hooks` mechanisms.
|
a more functional approach, in particular, with the new `hooks` mechanisms.
|
||||||
|
|
||||||
This has some advantages and disadvantages. But the end result is that React
|
This has some advantages and disadvantages. But the end result is that React
|
||||||
and Vue both offers multiple different ways of defining new components. In
|
and Vue both offers multiple different ways of defining new components. In
|
||||||
contrast, Owl has only one mechanism: class-based components. We believe that Owl
|
contrast, Owl has only one mechanism: class-based components. We believe that Owl
|
||||||
components are fast enough for all our usecases, and making it as simple as
|
components are fast enough for all our usecases, and making it as simple as
|
||||||
possible for developers is more valuable (for us).
|
possible for developers is more valuable (for us).
|
||||||
@@ -233,4 +233,4 @@ class Counter extends owl.ConnectedComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const counter = new Counter({ store, qweb });
|
const counter = new Counter({ store, qweb });
|
||||||
```
|
```
|
||||||
|
|||||||
+38
-12
@@ -72,7 +72,7 @@ Note that this code is written in ESNext style, so it will only run on the
|
|||||||
latest browsers without a transpilation step.
|
latest browsers without a transpilation step.
|
||||||
|
|
||||||
This example show how a component should be defined: it simply subclasses the
|
This example show how a component should be defined: it simply subclasses the
|
||||||
Component class. If no `template` key is defined, then
|
Component class. If no static `template` key is defined, then
|
||||||
Owl will use the component's name as template name. Here,
|
Owl will use the component's name as template name. Here,
|
||||||
a state object is defined. It is not mandatory to use the state object, but it
|
a state object is defined. It is not mandatory to use the state object, but it
|
||||||
is certainly encouraged. The state object is [observed](observer.md), and any
|
is certainly encouraged. The state object is [observed](observer.md), and any
|
||||||
@@ -96,9 +96,6 @@ find a template with the component name (or one of its ancestor).
|
|||||||
|
|
||||||
- **`env`** (Object): the component environment, which contains a QWeb instance.
|
- **`env`** (Object): the component environment, which contains a QWeb instance.
|
||||||
|
|
||||||
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render
|
|
||||||
the component.
|
|
||||||
|
|
||||||
- **`state`** (Object): this is the location of the component's state, if there is
|
- **`state`** (Object): this is the location of the component's state, if there is
|
||||||
any. After the willStart method, the `state` property is observed, and each
|
any. After the willStart method, the `state` property is observed, and each
|
||||||
change will cause the component to rerender itself.
|
change will cause the component to rerender itself.
|
||||||
@@ -113,7 +110,20 @@ find a template with the component name (or one of its ancestor).
|
|||||||
|
|
||||||
### Static Properties
|
### Static Properties
|
||||||
|
|
||||||
- **`props`** (Object, optional): if given, this is an object that describes the
|
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render the component. Note that there is a helper `xml` to
|
||||||
|
make it easy to define an inline template.
|
||||||
|
|
||||||
|
* **`components`** (Object, optional): if given, this is an object that contains
|
||||||
|
the classes of any sub components needed by the template. This is the main way
|
||||||
|
used by Owl to be able to create sub components.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class ParentComponent extends owl.Component {
|
||||||
|
static components = { SubComponent };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* **`props`** (Object, optional): if given, this is an object that describes the
|
||||||
type and shape of the (actual) props given to the component. If Owl mode is
|
type and shape of the (actual) props given to the component. If Owl mode is
|
||||||
`dev`, this will be used to validate the props each time the component is
|
`dev`, this will be used to validate the props each time the component is
|
||||||
created/updated. See [Props Validation](#props-validation) for more information.
|
created/updated. See [Props Validation](#props-validation) for more information.
|
||||||
@@ -127,7 +137,7 @@ find a template with the component name (or one of its ancestor).
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
* **`defaultProps`** (Object, optional): if given, this object define default
|
- **`defaultProps`** (Object, optional): if given, this object define default
|
||||||
values for (top-level) props. Whenever `props` are given to the object, they
|
values for (top-level) props. Whenever `props` are given to the object, they
|
||||||
will be altered to add default value (if missing). Note that it does not
|
will be altered to add default value (if missing). Note that it does not
|
||||||
change the initial object, a new object will be created instead.
|
change the initial object, a new object will be created instead.
|
||||||
@@ -420,7 +430,7 @@ to be capitalized.
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
class ParentComponent extends owl.Component {
|
class ParentComponent extends owl.Component {
|
||||||
components = { MyComponent: MyComponent};
|
static components = { MyComponent: MyComponent};
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -442,16 +452,16 @@ that the template can access `state`, `props`, `env`, or any methods defined in
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
class ParentComponent {
|
class ParentComponent {
|
||||||
components = { ChildComponent };
|
static components = { ChildComponent };
|
||||||
state = { val: 4 };
|
state = { val: 4 };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Whenever the template is rendered, it will automatically create the subcomponent
|
Whenever the template is rendered, it will automatically create the subcomponent
|
||||||
`ChildComponent` at the correct place. It needs to find the reference to the
|
`ChildComponent` at the correct place. It needs to find the reference to the
|
||||||
actual component class in the special `components` key, or the class registered in
|
actual component class in the special static `components` key, or the class registered in
|
||||||
QWeb's global registry (see `register` function of QWeb). It first looks inside
|
QWeb's global registry (see `register` function of QWeb). It first looks inside
|
||||||
the local `components` key, then fallbacks on the global registry.
|
the static `components` key, then fallbacks on the global registry.
|
||||||
|
|
||||||
_Props_: In this example, the child component will receive the object `{count: 4}` in its
|
_Props_: In this example, the child component will receive the object `{count: 4}` in its
|
||||||
constructor. This will be assigned to the `props` variable, which can be accessed
|
constructor. This will be assigned to the `props` variable, which can be accessed
|
||||||
@@ -461,7 +471,8 @@ the sub component will also be updated automatically.
|
|||||||
Note that there are some restrictions on prop names: `class`, `style` and any
|
Note that there are some restrictions on prop names: `class`, `style` and any
|
||||||
string which starts with `t-` are not allowed.
|
string which starts with `t-` are not allowed.
|
||||||
|
|
||||||
The `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
|
It is not common, but sometimes we need a dynamic component name and/or dynamic props. In this case,
|
||||||
|
the `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-name="ParentComponent">
|
<div t-name="ParentComponent">
|
||||||
@@ -471,11 +482,26 @@ The `t-component` directive can also be used to accept dynamic values with strin
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
class ParentComponent {
|
class ParentComponent {
|
||||||
components = { ChildComponent1, ChildComponent2 };
|
static components = { ChildComponent1, ChildComponent2 };
|
||||||
state = { id: 1 };
|
state = { id: 1 };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
And the `t-props` directive can be used to specify totally dynamic props:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="ParentComponent">
|
||||||
|
<Child t-props="some.obj"/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
class ParentComponent {
|
||||||
|
static components = { Child };
|
||||||
|
some = { obj: { a: 1, b: 2 } };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**CSS and style:** there is some specific support to allow the parent to declare
|
**CSS and style:** there is some specific support to allow the parent to declare
|
||||||
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
|
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
|
||||||
root component element.
|
root component element.
|
||||||
|
|||||||
+1
-1
@@ -73,9 +73,9 @@ Let us now add the javascript to make it work, in `app.js`:
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
class ClickCounter extends owl.Component {
|
class ClickCounter extends owl.Component {
|
||||||
|
static template = "clickcounter";
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
this.template = "clickcounter";
|
|
||||||
this.state = { value: 0 };
|
this.state = { value: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-13
@@ -65,16 +65,16 @@ We present here a list of all standard QWeb directives:
|
|||||||
The component system in Owl requires additional directives, to express various
|
The component system in Owl requires additional directives, to express various
|
||||||
needs. Here is a list of all Owl specific directives:
|
needs. Here is a list of all Owl specific directives:
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| ------------------------------------------- | ----------------------------------------------------------------------------------- |
|
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- |
|
||||||
| `t-component`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
|
| `t-component`, `t-props`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
|
||||||
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
||||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
|
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
|
||||||
| `t-on-*` | [Event handling](component.md#event-handling) |
|
| `t-on-*` | [Event handling](component.md#event-handling) |
|
||||||
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
||||||
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
|
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
|
||||||
| `t-slot` | [Rendering a slot](component.md#slots) |
|
| `t-slot` | [Rendering a slot](component.md#slots) |
|
||||||
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
||||||
|
|
||||||
## QWeb Engine
|
## QWeb Engine
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ It's API is quite simple:
|
|||||||
qweb.addTemplate("mytemplate", "<div>hello</div>");
|
qweb.addTemplate("mytemplate", "<div>hello</div>");
|
||||||
```
|
```
|
||||||
|
|
||||||
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply return whenever a template is added for a second time. Otherwise, `QWeb` will crash.
|
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply return whenever a template is added for a second time. Otherwise, `QWeb` will crash.
|
||||||
|
|
||||||
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
|
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
|
||||||
attribute).
|
attribute).
|
||||||
@@ -129,14 +129,23 @@ It's API is quite simple:
|
|||||||
const str = qweb.renderToString("someTemplate", somecontext);
|
const str = qweb.renderToString("someTemplate", somecontext);
|
||||||
```
|
```
|
||||||
|
|
||||||
- **`register(name, Component)`**: static function to register an OWL Component
|
- **`registerTemplate(name, template)`**: static function to register an global
|
||||||
|
QWeb template. This is useful for commonly used components accross the
|
||||||
|
application, and for making a template available to an application without
|
||||||
|
having a reference to the actual QWeb instance.
|
||||||
|
|
||||||
|
```js
|
||||||
|
QWeb.registerTemplate("mytemplate", `<div>some template`);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`registerComponent(name, Component)`**: static function to register an OWL Component
|
||||||
to QWeb's global registry. Globally registered Components can be used in
|
to QWeb's global registry. Globally registered Components can be used in
|
||||||
templates (see the `t-component` directive). This is useful for commonly used
|
templates (see the `t-component` directive). This is useful for commonly used
|
||||||
components accross the application.
|
components accross the application.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
class Dialog extends owl.Component { ... }
|
class Dialog extends owl.Component { ... }
|
||||||
QWeb.register("Dialog", Dialog);
|
QWeb.registerComponent("Dialog", Dialog);
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -3,7 +3,7 @@
|
|||||||
## Owl Content
|
## Owl Content
|
||||||
|
|
||||||
Owl is a javascript library that contains some core classes and function to help
|
Owl is a javascript library that contains some core classes and function to help
|
||||||
build applications. Here is a complete representation of its content:
|
build applications. Here is a complete representation of its content:
|
||||||
|
|
||||||
```
|
```
|
||||||
owl
|
owl
|
||||||
@@ -14,10 +14,13 @@ owl
|
|||||||
Observer
|
Observer
|
||||||
router
|
router
|
||||||
Link
|
Link
|
||||||
|
RouteComponent
|
||||||
Router
|
Router
|
||||||
store
|
store
|
||||||
Store
|
Store
|
||||||
ConnectedComponent
|
ConnectedComponent
|
||||||
|
tags
|
||||||
|
xml
|
||||||
utils
|
utils
|
||||||
debounce
|
debounce
|
||||||
escape
|
escape
|
||||||
@@ -35,6 +38,7 @@ owl
|
|||||||
- [QWeb](qweb.md)
|
- [QWeb](qweb.md)
|
||||||
- [Router](router.md)
|
- [Router](router.md)
|
||||||
- [Store](store.md)
|
- [Store](store.md)
|
||||||
|
- [Tags](tags.md)
|
||||||
- [Utils](utils.md)
|
- [Utils](utils.md)
|
||||||
- [Virtual DOM](vdom.md)
|
- [Virtual DOM](vdom.md)
|
||||||
|
|
||||||
@@ -46,4 +50,4 @@ owl
|
|||||||
|
|
||||||
- [Comparison with React/Vue](comparison.md)
|
- [Comparison with React/Vue](comparison.md)
|
||||||
- [Tooling](tooling.md)
|
- [Tooling](tooling.md)
|
||||||
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
|
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
|
||||||
|
|||||||
+7
-8
@@ -8,7 +8,7 @@
|
|||||||
- [Route Definition](#route-definition)
|
- [Route Definition](#route-definition)
|
||||||
- [Router](#router)
|
- [Router](#router)
|
||||||
- [Navigation Guards](#navigation-guards)
|
- [Navigation Guards](#navigation-guards)
|
||||||
- [`t-routecomponent`](#t-routecomponent)
|
- [RouteComponent](#routecomponent)
|
||||||
- [Link](#link)
|
- [Link](#link)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
@@ -26,8 +26,8 @@ The Owl router support the following features:
|
|||||||
- route redirection
|
- route redirection
|
||||||
- navigation guards
|
- navigation guards
|
||||||
- parameterized routes
|
- parameterized routes
|
||||||
- a `<Link>` component
|
- a `<Link/>` component
|
||||||
- a `t-routecomponent` directive
|
- a `<RouteComponent/>` component
|
||||||
|
|
||||||
Note that it is still in early stage of developments, and there are probably
|
Note that it is still in early stage of developments, and there are probably
|
||||||
still some issues.
|
still some issues.
|
||||||
@@ -125,7 +125,7 @@ The router also has a `navigate` method, useful to programmatically change the
|
|||||||
application to another state (and the url):
|
application to another state (and the url):
|
||||||
|
|
||||||
```js
|
```js
|
||||||
router.navigate({to: 'USER', params: {id: 51}});
|
router.navigate({ to: "USER", params: { id: 51 } });
|
||||||
```
|
```
|
||||||
|
|
||||||
### Navigation Guards
|
### Navigation Guards
|
||||||
@@ -148,19 +148,18 @@ async function protectRoute({ env, to }) {
|
|||||||
A navigation guard is a function that returns a promise, which either resolves
|
A navigation guard is a function that returns a promise, which either resolves
|
||||||
to `true` (the navigation is accepted), or to another destination object.
|
to `true` (the navigation is accepted), or to another destination object.
|
||||||
|
|
||||||
### `t-routecomponent`
|
### `RouteComponent`
|
||||||
|
|
||||||
The `t-routecomponent` directive directs Owl to render the component associated
|
The `RouteComponent` component directs Owl to render the component associated
|
||||||
to the currently active route (if any):
|
to the currently active route (if any):
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-name="App">
|
<div t-name="App">
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<t t-routecomponent="1"/>
|
<RouteComponent />
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### `Link`
|
### `Link`
|
||||||
|
|
||||||
The `Link` component is a Owl component which render as a `<a>` tag with any
|
The `Link` component is a Owl component which render as a `<a>` tag with any
|
||||||
|
|||||||
+1
-1
@@ -103,7 +103,7 @@ const actions = {
|
|||||||
try {
|
try {
|
||||||
const loginInfo = await doSomeRPC("/login/", info);
|
const loginInfo = await doSomeRPC("/login/", info);
|
||||||
state.loginState = loginInfo;
|
state.loginState = loginInfo;
|
||||||
} catch {
|
} catch (e) {
|
||||||
state.loginState = "error";
|
state.loginState = "error";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
# 🦉 Tags 🦉
|
||||||
|
|
||||||
|
Tags are very small helper to make it easy to write inline templates. There is
|
||||||
|
only one currently available tag: `xml`, but we plan to add other tags later,
|
||||||
|
such as a `css` tag, which will be used to write single file components.
|
||||||
|
|
||||||
|
## XML tag
|
||||||
|
|
||||||
|
Without tags, creating a standalone component would look like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { Component } from 'owl'
|
||||||
|
|
||||||
|
const name = 'some-unique-name';
|
||||||
|
const template = `
|
||||||
|
<div>
|
||||||
|
<span t-if="somecondition">text</span>
|
||||||
|
<button t-on-click="someMethod">Click</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
QWeb.registerTemplate(name, template);
|
||||||
|
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static template = name;
|
||||||
|
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With tags, this process is slightly simplified. The name is uniquely generated,
|
||||||
|
and the template is automatically registered:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { Component } from 'owl'
|
||||||
|
import { xml } from 'owl/tags'
|
||||||
|
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<span t-if="somecondition">text</span>
|
||||||
|
<button t-on-click="someMethod">Click</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
+1
-1
@@ -38,7 +38,7 @@ Note that templates compiled with the `prod` settings will not be recompiled.
|
|||||||
So, changing this setting is best done at startup.
|
So, changing this setting is best done at startup.
|
||||||
|
|
||||||
An important job done by the `dev` mode is to validate props for each component
|
An important job done by the `dev` mode is to validate props for each component
|
||||||
creation and update. Also, extra props will cause an error.
|
creation and update. Also, extra props will cause an error.
|
||||||
|
|
||||||
## Playground
|
## Playground
|
||||||
|
|
||||||
|
|||||||
+1
-3
@@ -18,9 +18,7 @@ not ready yet, resolved directly otherwise). If called with a callback as
|
|||||||
argument, it executes it as soon as the DOM ready (or directly).
|
argument, it executes it as soon as the DOM ready (or directly).
|
||||||
|
|
||||||
```js
|
```js
|
||||||
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([
|
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([templates]) {
|
||||||
templates
|
|
||||||
]) {
|
|
||||||
const qweb = new owl.QWeb(templates);
|
const qweb = new owl.QWeb(templates);
|
||||||
const app = new App({ qweb });
|
const app = new App({ qweb });
|
||||||
app.mount(document.body);
|
app.mount(document.body);
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "owl",
|
"name": "owl-framework",
|
||||||
"version": "0.19.0",
|
"version": "0.21.0",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -21,11 +21,12 @@
|
|||||||
"url": "git+https://github.com/odoo/owl.git"
|
"url": "git+https://github.com/odoo/owl.git"
|
||||||
},
|
},
|
||||||
"author": "Odoo",
|
"author": "Odoo",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "LGPL",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/odoo/owl/issues"
|
"url": "https://github.com/odoo/owl/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/odoo/owl#readme",
|
"homepage": "https://github.com/odoo/owl#readme",
|
||||||
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^23.3.12",
|
"@types/jest": "^23.3.12",
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
@@ -42,7 +43,6 @@
|
|||||||
"typescript": "^3.2.2",
|
"typescript": "^3.2.2",
|
||||||
"uglify-es": "^3.3.9"
|
"uglify-es": "^3.3.9"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
|
||||||
"jest": {
|
"jest": {
|
||||||
"roots": [
|
"roots": [
|
||||||
"<rootDir>/src",
|
"<rootDir>/src",
|
||||||
|
|||||||
+15
-21
@@ -70,12 +70,6 @@ interface Internal<T extends Env, Props> {
|
|||||||
classObj: { [key: string]: boolean } | null;
|
classObj: { [key: string]: boolean } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a component does not define explicitely a template
|
|
||||||
// key, it needs to find a template with its name (or a parent's). This is
|
|
||||||
// qweb dependant, so we need a place to store this information indexed by
|
|
||||||
// qweb instances.
|
|
||||||
const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Component
|
// Component
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -83,7 +77,8 @@ let nextId = 1;
|
|||||||
|
|
||||||
export class Component<T extends Env, Props extends {}, State extends {}> {
|
export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||||
readonly __owl__: Internal<Env, Props>;
|
readonly __owl__: Internal<Env, Props>;
|
||||||
template?: string;
|
static template?: string | null = null;
|
||||||
|
static _template?: string | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `el` is the root element of the component. Note that it could be null:
|
* The `el` is the root element of the component. Note that it could be null:
|
||||||
@@ -92,6 +87,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
get el(): HTMLElement | null {
|
get el(): HTMLElement | null {
|
||||||
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
||||||
}
|
}
|
||||||
|
static components = {};
|
||||||
|
|
||||||
env: T;
|
env: T;
|
||||||
state?: State;
|
state?: State;
|
||||||
@@ -526,30 +522,28 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
return Promise.resolve(h("div"));
|
return Promise.resolve(h("div"));
|
||||||
}
|
}
|
||||||
const qweb = this.env.qweb;
|
const qweb = this.env.qweb;
|
||||||
if (!this.template) {
|
let p = (<any>this).constructor;
|
||||||
let tmap = TEMPLATE_MAP[qweb.id];
|
// console.warn(p, p.template, p._template, 'template' in p, p.hasOwnProperty('template'))
|
||||||
if (!tmap) {
|
if (!p.hasOwnProperty("_template")) {
|
||||||
tmap = {};
|
if (p.template) {
|
||||||
TEMPLATE_MAP[qweb.id] = tmap;
|
p._template = p.template;
|
||||||
}
|
|
||||||
let p = (<any>this).constructor;
|
|
||||||
let name: string = p.name;
|
|
||||||
let template = tmap[name];
|
|
||||||
if (template) {
|
|
||||||
this.template = template;
|
|
||||||
} else {
|
} else {
|
||||||
|
// here, the component and none of its superclasses defines a static `template`
|
||||||
|
// key. So we fall back on looking for a template matching its name (or
|
||||||
|
// one of its subclass).
|
||||||
|
|
||||||
|
let template: string;
|
||||||
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
|
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
|
||||||
p = p.__proto__;
|
p = p.__proto__;
|
||||||
}
|
}
|
||||||
if (p === Component) {
|
if (p === Component) {
|
||||||
throw new Error(`Could not find template for component "${this.constructor.name}"`);
|
throw new Error(`Could not find template for component "${this.constructor.name}"`);
|
||||||
} else {
|
} else {
|
||||||
tmap[name] = template;
|
p._template = template;
|
||||||
this.template = template;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
__owl__.render = qweb.render.bind(qweb, this.template);
|
__owl__.render = qweb.render.bind(qweb, p._template);
|
||||||
this.__observeState();
|
this.__observeState();
|
||||||
|
|
||||||
return this.__render(false, [], scope, vars);
|
return this.__render(false, [], scope, vars);
|
||||||
|
|||||||
+27
-21
@@ -194,6 +194,7 @@ QWeb.addDirective({
|
|||||||
ctx.rootContext.shouldDefineParent = true;
|
ctx.rootContext.shouldDefineParent = true;
|
||||||
ctx.rootContext.shouldDefineUtils = true;
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
|
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
|
||||||
|
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
|
||||||
let async = node.getAttribute("t-asyncroot") ? true : false;
|
let async = node.getAttribute("t-asyncroot") ? true : false;
|
||||||
|
|
||||||
// t-on- events and t-transition
|
// t-on- events and t-transition
|
||||||
@@ -238,23 +239,23 @@ QWeb.addDirective({
|
|||||||
if (key) {
|
if (key) {
|
||||||
// we bind a variable to the key (could be a complex expression, so we
|
// we bind a variable to the key (could be a complex expression, so we
|
||||||
// want to evaluate it only once)
|
// want to evaluate it only once)
|
||||||
ctx.addLine(`let key${keyID} = ${key};`);
|
ctx.addLine(`let key${keyID} = 'key' + ${key};`);
|
||||||
}
|
}
|
||||||
ctx.addLine(`let def${defID};`);
|
ctx.addLine(`let def${defID};`);
|
||||||
let templateID = key
|
let templateID = key
|
||||||
? `key${keyID}`
|
? `key${keyID}`
|
||||||
: ctx.inLoop
|
: ctx.inLoop
|
||||||
? `String(-${componentID} - i)`
|
? ctx.currentKey
|
||||||
|
? `String(${ctx.currentKey} + '_k_' + i + '_c_' + ${componentID} )`
|
||||||
|
: `String(-${componentID} - i)`
|
||||||
: String(componentID);
|
: String(componentID);
|
||||||
if (ctx.allowMultipleRoots) {
|
if (ctx.allowMultipleRoots) {
|
||||||
// necessary to prevent collisions
|
templateID = `"_slot_${templateID}"`;
|
||||||
if (!key && ctx.inLoop) {
|
}
|
||||||
let id = ctx.generateID();
|
if (key || ctx.inLoop) {
|
||||||
ctx.addLine(`let template${id} = "_slot_" + String(-${componentID} - i)`);
|
let id = ctx.generateID();
|
||||||
templateID = `template${id}`;
|
ctx.addLine(`let templateId${id} = ${templateID};`);
|
||||||
} else {
|
templateID = `templateId${id}`;
|
||||||
templateID = `"_slot_${templateID}"`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let ref = node.getAttribute("t-ref");
|
let ref = node.getAttribute("t-ref");
|
||||||
@@ -372,7 +373,12 @@ QWeb.addDirective({
|
|||||||
ctx.addLine(`result = vn${id};`);
|
ctx.addLine(`result = vn${id};`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.addLine(`let props${componentID} = {${propStr}};`);
|
if (hasDynamicProps) {
|
||||||
|
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
|
||||||
|
ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`);
|
||||||
|
} else {
|
||||||
|
ctx.addLine(`let props${componentID} = {${propStr}};`);
|
||||||
|
}
|
||||||
ctx.addIf(
|
ctx.addIf(
|
||||||
`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`
|
`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`
|
||||||
);
|
);
|
||||||
@@ -388,7 +394,7 @@ QWeb.addDirective({
|
|||||||
// new component
|
// new component
|
||||||
ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`);
|
ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`);
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`let W${componentID} = context.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
|
`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
|
||||||
);
|
);
|
||||||
// maybe only do this in dev mode...
|
// maybe only do this in dev mode...
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
@@ -413,7 +419,7 @@ QWeb.addDirective({
|
|||||||
|
|
||||||
const clone = <Element>node.cloneNode(true);
|
const clone = <Element>node.cloneNode(true);
|
||||||
const slotNodes = clone.querySelectorAll("[t-set]");
|
const slotNodes = clone.querySelectorAll("[t-set]");
|
||||||
const slotId = qweb.nextSlotId++;
|
const slotId = QWeb.nextSlotId++;
|
||||||
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
|
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
|
||||||
if (slotNodes.length) {
|
if (slotNodes.length) {
|
||||||
for (let i = 0, length = slotNodes.length; i < length; i++) {
|
for (let i = 0, length = slotNodes.length; i < length; i++) {
|
||||||
@@ -422,7 +428,7 @@ QWeb.addDirective({
|
|||||||
const key = slotNode.getAttribute("t-set")!;
|
const key = slotNode.getAttribute("t-set")!;
|
||||||
slotNode.removeAttribute("t-set");
|
slotNode.removeAttribute("t-set");
|
||||||
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
|
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
|
||||||
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
|
QWeb.slots[`${slotId}_${key}`] = slotFn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (clone.childNodes.length) {
|
if (clone.childNodes.length) {
|
||||||
@@ -431,7 +437,7 @@ QWeb.addDirective({
|
|||||||
t.appendChild(child);
|
t.appendChild(child);
|
||||||
}
|
}
|
||||||
const slotFn = qweb._compile(`slot_default_template`, t, ctx);
|
const slotFn = qweb._compile(`slot_default_template`, t, ctx);
|
||||||
qweb.slots[`${slotId}_default`] = slotFn.bind(qweb);
|
QWeb.slots[`${slotId}_default`] = slotFn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,18 +455,18 @@ QWeb.addDirective({
|
|||||||
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
|
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
|
||||||
}
|
}
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});`
|
`def${defID} = def${defID}.then(vnode=>{if (w${componentID}.__owl__.isDestroyed){return}${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});`
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.addElse();
|
ctx.addElse();
|
||||||
// need to update component
|
// need to update component
|
||||||
let patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
|
let patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
|
||||||
if (keepAlive) {
|
if (keepAlive) {
|
||||||
// if we have t-keepalive="1", the component could be unmounted, but then
|
// if we have t-keepalive="1", the component could be unmounted, but then
|
||||||
// we __updateProps is called. This is ok, but we do not want to call
|
// we __updateProps is called. This is ok, but we do not want to call
|
||||||
// the willPatch/patched hooks of the component in this case, so we
|
// the willPatch/patched hooks of the component in this case, so we
|
||||||
// disable the patch queue
|
// disable the patch queue
|
||||||
patchQueueCode = `w${componentID}.__owl__.isMounted ? ${patchQueueCode} : []`;
|
patchQueueCode = `w${componentID}.__owl__.isMounted ? ${patchQueueCode} : []`;
|
||||||
}
|
}
|
||||||
if (QWeb.dev) {
|
if (QWeb.dev) {
|
||||||
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
|
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export class Observer {
|
|||||||
|
|
||||||
notifyCB() {}
|
notifyCB() {}
|
||||||
async notifyChange() {
|
async notifyChange() {
|
||||||
|
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
if (this.dirty) {
|
if (this.dirty) {
|
||||||
|
|||||||
+4
-2
@@ -10,16 +10,18 @@ import { QWeb } from "./qweb/index";
|
|||||||
import { ConnectedComponent } from "./store/connected_component";
|
import { ConnectedComponent } from "./store/connected_component";
|
||||||
import { Store } from "./store/store";
|
import { Store } from "./store/store";
|
||||||
import * as _utils from "./utils";
|
import * as _utils from "./utils";
|
||||||
|
import * as _tags from "./tags";
|
||||||
import { Link } from "./router/Link";
|
import { Link } from "./router/Link";
|
||||||
|
import { RouteComponent } from "./router/RouteComponent";
|
||||||
import { Router } from "./router/Router";
|
import { Router } from "./router/Router";
|
||||||
|
|
||||||
export { Component } from "./component/component";
|
export { Component } from "./component/component";
|
||||||
export { QWeb };
|
export { QWeb };
|
||||||
export const core = { EventBus, Observer };
|
export const core = { EventBus, Observer };
|
||||||
export const router = { Router, Link };
|
export const router = { Router, RouteComponent, Link };
|
||||||
export const store = { Store, ConnectedComponent };
|
export const store = { Store, ConnectedComponent };
|
||||||
export const utils = _utils;
|
export const utils = _utils;
|
||||||
|
export const tags = _tags;
|
||||||
export const __info__ = {};
|
export const __info__ = {};
|
||||||
|
|
||||||
Object.defineProperty(__info__, "mode", {
|
Object.defineProperty(__info__, "mode", {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ 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}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let fragID = ctx.generateID();
|
let fragID = ctx.generateID();
|
||||||
@@ -82,6 +83,7 @@ QWeb.addDirective({
|
|||||||
if (node.nodeName !== "t") {
|
if (node.nodeName !== "t") {
|
||||||
let nodeID = qweb._compileGenericNode(node, ctx);
|
let nodeID = qweb._compileGenericNode(node, ctx);
|
||||||
ctx = ctx.withParent(nodeID);
|
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));
|
||||||
@@ -96,6 +98,7 @@ QWeb.addDirective({
|
|||||||
if (node.nodeName !== "t") {
|
if (node.nodeName !== "t") {
|
||||||
let nodeID = qweb._compileGenericNode(node, ctx);
|
let nodeID = qweb._compileGenericNode(node, ctx);
|
||||||
ctx = ctx.withParent(nodeID);
|
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);
|
||||||
@@ -198,12 +201,55 @@ QWeb.addDirective({
|
|||||||
// extract variables from nodecopy
|
// extract variables from nodecopy
|
||||||
const tempCtx = new Context();
|
const tempCtx = new Context();
|
||||||
tempCtx.nextID = ctx.rootContext.nextID;
|
tempCtx.nextID = ctx.rootContext.nextID;
|
||||||
|
tempCtx.allowMultipleRoots = true;
|
||||||
qweb._compileNode(nodeCopy, tempCtx);
|
qweb._compileNode(nodeCopy, tempCtx);
|
||||||
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
|
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
|
||||||
ctx.rootContext.nextID = tempCtx.nextID;
|
ctx.rootContext.nextID = tempCtx.nextID;
|
||||||
|
|
||||||
|
const templateMap = Object.create(ctx.templates);
|
||||||
// open new scope, if necessary
|
// open new scope, if necessary
|
||||||
const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
|
const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
|
||||||
|
|
||||||
|
// compile sub template
|
||||||
|
let subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
|
||||||
|
subCtx = subCtx.subContext("templates", templateMap);
|
||||||
|
|
||||||
|
if (templateMap[subTemplate]) {
|
||||||
|
// OUCH, IT IS A RECURSIVE TEMPLATE SITUATION...
|
||||||
|
// This is a tricky situation... We obviously cannot inline the compiled
|
||||||
|
// template. So, what we need to do is to compile it, and make sure we
|
||||||
|
// properly transfer everything from the current scope to the sub template.
|
||||||
|
ctx.rootContext.shouldTrackScope = true;
|
||||||
|
ctx.rootContext.shouldDefineOwner = true;
|
||||||
|
let subTemplateName;
|
||||||
|
if (ctx.hasParentWidget) {
|
||||||
|
subTemplateName = ctx.templateName;
|
||||||
|
} else {
|
||||||
|
subTemplateName = `__${ctx.generateID()}`;
|
||||||
|
subCtx.variables = {};
|
||||||
|
let id = 0;
|
||||||
|
for (let v in vars) {
|
||||||
|
subCtx.variables[v] = vars[v];
|
||||||
|
(vars[v] as any).id = `_v${id++}`;
|
||||||
|
}
|
||||||
|
const subTemplateFn = qweb._compile(subTemplateName, nodeTemplate.elem, subCtx);
|
||||||
|
qweb.recursiveFns[subTemplateName] = subTemplateFn;
|
||||||
|
}
|
||||||
|
let varCode = `{}`;
|
||||||
|
if (Object.keys(vars).length) {
|
||||||
|
let id = 0;
|
||||||
|
const content = Object.values(vars)
|
||||||
|
.map((v: any) => `_v${id++}: ${v.expr}`)
|
||||||
|
.join(",");
|
||||||
|
varCode = `{${content}}`;
|
||||||
|
}
|
||||||
|
ctx.addLine(
|
||||||
|
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: ${varCode}, scope}));`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
templateMap[subTemplate] = true;
|
||||||
|
|
||||||
if (hasNewVariables) {
|
if (hasNewVariables) {
|
||||||
ctx.addLine("{");
|
ctx.addLine("{");
|
||||||
ctx.indent();
|
ctx.indent();
|
||||||
@@ -216,10 +262,6 @@ QWeb.addDirective({
|
|||||||
// todo: handle XML variables...
|
// todo: handle XML variables...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// compile sub template
|
|
||||||
const subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
|
|
||||||
|
|
||||||
qweb._compileNode(nodeTemplate.elem, subCtx);
|
qweb._compileNode(nodeTemplate.elem, subCtx);
|
||||||
|
|
||||||
// close new scope
|
// close new scope
|
||||||
@@ -283,9 +325,7 @@ QWeb.addDirective({
|
|||||||
}
|
}
|
||||||
if (shouldWarn) {
|
if (shouldWarn) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Directive t-foreach should always be used with a t-key! (in template: '${
|
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
|
||||||
ctx.templateName
|
|
||||||
}')`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
nodeCopy.removeAttribute("t-foreach");
|
nodeCopy.removeAttribute("t-foreach");
|
||||||
|
|||||||
+8
-1
@@ -19,7 +19,7 @@ export class Context {
|
|||||||
shouldDefineParent: boolean = false;
|
shouldDefineParent: boolean = false;
|
||||||
shouldDefineQWeb: boolean = false;
|
shouldDefineQWeb: boolean = false;
|
||||||
shouldDefineUtils: boolean = false;
|
shouldDefineUtils: boolean = false;
|
||||||
shouldDefineResult: boolean = false;
|
shouldDefineResult: boolean = true;
|
||||||
shouldProtectContext: boolean = false;
|
shouldProtectContext: boolean = false;
|
||||||
shouldTrackScope: boolean = false;
|
shouldTrackScope: boolean = false;
|
||||||
inLoop: boolean = false;
|
inLoop: boolean = false;
|
||||||
@@ -28,10 +28,14 @@ export class Context {
|
|||||||
allowMultipleRoots: boolean = false;
|
allowMultipleRoots: boolean = false;
|
||||||
hasParentWidget: boolean = false;
|
hasParentWidget: boolean = false;
|
||||||
scopeVars: any[] = [];
|
scopeVars: any[] = [];
|
||||||
|
currentKey: string = "";
|
||||||
|
lastNodeKey: string = ""; // temp variable to communicate to previous caller
|
||||||
|
templates: { [key: string]: boolean } = {};
|
||||||
|
|
||||||
constructor(name?: string) {
|
constructor(name?: string) {
|
||||||
this.rootContext = this;
|
this.rootContext = this;
|
||||||
this.templateName = name || "noname";
|
this.templateName = name || "noname";
|
||||||
|
this.templates[this.templateName] = true;
|
||||||
this.addLine("var h = this.h;");
|
this.addLine("var h = this.h;");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +93,9 @@ export class Context {
|
|||||||
if (!this.rootContext.rootNode) {
|
if (!this.rootContext.rootNode) {
|
||||||
this.rootContext.rootNode = node;
|
this.rootContext.rootNode = node;
|
||||||
}
|
}
|
||||||
|
if (!this.parentNode) {
|
||||||
|
this.addLine(`result = vn${node};`);
|
||||||
|
}
|
||||||
return this.subContext("parentNode", node);
|
return this.subContext("parentNode", node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-13
@@ -227,12 +227,12 @@ QWeb.addDirective({
|
|||||||
atNodeEncounter({ ctx, value }): boolean {
|
atNodeEncounter({ ctx, value }): boolean {
|
||||||
const slotKey = ctx.generateID();
|
const slotKey = ctx.generateID();
|
||||||
ctx.rootContext.shouldDefineOwner = true;
|
ctx.rootContext.shouldDefineOwner = true;
|
||||||
ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`);
|
ctx.addLine(
|
||||||
|
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
|
||||||
|
);
|
||||||
ctx.addIf(`slot${slotKey}`);
|
ctx.addIf(`slot${slotKey}`);
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${
|
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: extra.vars, parent: owner}));`
|
||||||
ctx.parentNode
|
|
||||||
}, vars: extra.vars, parent: owner}));`
|
|
||||||
);
|
);
|
||||||
ctx.closeIf();
|
ctx.closeIf();
|
||||||
return true;
|
return true;
|
||||||
@@ -250,31 +250,33 @@ QWeb.utils.toNumber = function(val: string): number | string {
|
|||||||
QWeb.addDirective({
|
QWeb.addDirective({
|
||||||
name: "model",
|
name: "model",
|
||||||
priority: 42,
|
priority: 42,
|
||||||
atNodeCreation({ ctx, nodeID, value, node, fullName }) {
|
atNodeCreation({ ctx, nodeID, value, node, fullName, addNodeHook }) {
|
||||||
const type = node.getAttribute("type");
|
const type = node.getAttribute("type");
|
||||||
let handler;
|
let handler;
|
||||||
let event = fullName.includes(".lazy") ? "change" : "input";
|
let event = fullName.includes(".lazy") ? "change" : "input";
|
||||||
|
const expr = ctx.formatExpression(`state.${value}`);
|
||||||
if (node.tagName === "select") {
|
if (node.tagName === "select") {
|
||||||
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
|
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
||||||
|
addNodeHook("create", `n.elm.value=${expr};`);
|
||||||
event = "change";
|
event = "change";
|
||||||
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
|
handler = `(ev) => {${expr} = ev.target.value}`;
|
||||||
} else if (type === "checkbox") {
|
} else if (type === "checkbox") {
|
||||||
ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`);
|
ctx.addLine(`p${nodeID}.props = {checked: ${expr}};`);
|
||||||
handler = `(ev) => {context.state['${value}'] = ev.target.checked}`;
|
handler = `(ev) => {${expr} = ev.target.checked}`;
|
||||||
} else if (type === "radio") {
|
} else if (type === "radio") {
|
||||||
const nodeValue = node.getAttribute("value")!;
|
const nodeValue = node.getAttribute("value")!;
|
||||||
ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`);
|
ctx.addLine(`p${nodeID}.props = {checked:${expr} === '${nodeValue}'};`);
|
||||||
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
|
handler = `(ev) => {${expr} = ev.target.value}`;
|
||||||
event = "click";
|
event = "click";
|
||||||
} else {
|
} else {
|
||||||
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
|
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
||||||
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
|
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
|
||||||
let valueCode = `ev.target.value${trimCode}`;
|
let valueCode = `ev.target.value${trimCode}`;
|
||||||
if (fullName.includes(".number")) {
|
if (fullName.includes(".number")) {
|
||||||
ctx.rootContext.shouldDefineUtils = true;
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
valueCode = `utils.toNumber(${valueCode})`;
|
valueCode = `utils.toNumber(${valueCode})`;
|
||||||
}
|
}
|
||||||
handler = `(ev) => {context.state['${value}'] = ${valueCode}}`;
|
handler = `(ev) => {${expr} = ${valueCode}}`;
|
||||||
}
|
}
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
|
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
|
||||||
|
|||||||
+41
-21
@@ -1,6 +1,7 @@
|
|||||||
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 { Context } from "./context";
|
||||||
|
import { shallowEqual } from "../utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl QWeb Engine
|
* Owl QWeb Engine
|
||||||
@@ -92,14 +93,7 @@ const UTILS: Utils = {
|
|||||||
}
|
}
|
||||||
return expr;
|
return expr;
|
||||||
},
|
},
|
||||||
shallowEqual(p1, p2) {
|
shallowEqual
|
||||||
for (let k in p1) {
|
|
||||||
if (p1[k] !== p2[k]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseXML(xml: string): Document {
|
function parseXML(xml: string): Document {
|
||||||
@@ -131,13 +125,11 @@ function parseXML(xml: string): Document {
|
|||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextID = 1;
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// QWeb rendering engine
|
// QWeb rendering engine
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
export class QWeb extends EventBus {
|
export class QWeb extends EventBus {
|
||||||
templates: { [name: string]: Template } = {};
|
templates: { [name: string]: Template };
|
||||||
static utils = UTILS;
|
static utils = UTILS;
|
||||||
static components = Object.create(null);
|
static components = Object.create(null);
|
||||||
|
|
||||||
@@ -149,24 +141,29 @@ export class QWeb extends EventBus {
|
|||||||
};
|
};
|
||||||
static DIRECTIVES: Directive[] = [];
|
static DIRECTIVES: Directive[] = [];
|
||||||
|
|
||||||
|
static TEMPLATES: { [name: string]: Template } = {};
|
||||||
|
|
||||||
|
static nextId: number = 1;
|
||||||
|
|
||||||
h = h;
|
h = h;
|
||||||
// dev mode enables better error messages or more costly validations
|
// dev mode enables better error messages or more costly validations
|
||||||
static dev: boolean = false;
|
static dev: boolean = false;
|
||||||
|
|
||||||
// the id field is useful to be able to hash qweb instances. The current
|
|
||||||
// use case is that component's templates are qweb dependant, and need to be
|
|
||||||
// able to map a qweb instance to a template name.
|
|
||||||
id = nextID++;
|
|
||||||
|
|
||||||
// slots contains sub templates defined with t-set inside t-component nodes, and
|
// slots contains sub templates defined with t-set inside t-component nodes, and
|
||||||
// are meant to be used by the t-slot directive.
|
// are meant to be used by the t-slot directive.
|
||||||
slots = {};
|
static slots = {};
|
||||||
nextSlotId = 1;
|
static nextSlotId = 1;
|
||||||
|
|
||||||
|
// recursiveTemplates contains sub templates called with t-call, but which
|
||||||
|
// ends up in recursive situations. This is very similar to the slot situation,
|
||||||
|
// as in we need to propagate the scope.
|
||||||
|
recursiveFns = {};
|
||||||
|
|
||||||
isUpdating: boolean = false;
|
isUpdating: boolean = false;
|
||||||
|
|
||||||
constructor(data?: string) {
|
constructor(data?: string) {
|
||||||
super();
|
super();
|
||||||
|
this.templates = Object.create(QWeb.TEMPLATES);
|
||||||
if (data) {
|
if (data) {
|
||||||
this.addTemplates(data);
|
this.addTemplates(data);
|
||||||
}
|
}
|
||||||
@@ -184,13 +181,27 @@ export class QWeb extends EventBus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static register(name: string, Component: any) {
|
static registerComponent(name: string, Component: any) {
|
||||||
if (QWeb.components[name]) {
|
if (QWeb.components[name]) {
|
||||||
throw new Error(`Component '${name}' has already been registered`);
|
throw new Error(`Component '${name}' has already been registered`);
|
||||||
}
|
}
|
||||||
QWeb.components[name] = Component;
|
QWeb.components[name] = Component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register globally a template. All QWeb instances will obtain their
|
||||||
|
* templates from their own template map, and then, from the global static
|
||||||
|
* TEMPLATES property.
|
||||||
|
*/
|
||||||
|
static registerTemplate(name: string, template: string) {
|
||||||
|
if (QWeb.TEMPLATES[name]) {
|
||||||
|
throw new Error(`Template '${name}' has already been registered`);
|
||||||
|
}
|
||||||
|
const qweb = new QWeb();
|
||||||
|
qweb.addTemplate(name, template);
|
||||||
|
QWeb.TEMPLATES[name] = qweb.templates[name];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a template to the internal template map. Note that it is not
|
* Add a template to the internal template map. Note that it is not
|
||||||
* immediately compiled.
|
* immediately compiled.
|
||||||
@@ -212,7 +223,7 @@ export class QWeb extends EventBus {
|
|||||||
* template, with the name given by the t-name attribute.
|
* template, with the name given by the t-name attribute.
|
||||||
*/
|
*/
|
||||||
addTemplates(xmlstr: string | Document) {
|
addTemplates(xmlstr: string | Document) {
|
||||||
const doc = typeof xmlstr === 'string' ? parseXML(xmlstr) : xmlstr;
|
const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr;
|
||||||
const templates = doc.getElementsByTagName("templates")[0];
|
const templates = doc.getElementsByTagName("templates")[0];
|
||||||
if (!templates) {
|
if (!templates) {
|
||||||
return;
|
return;
|
||||||
@@ -327,12 +338,17 @@ export class QWeb extends EventBus {
|
|||||||
_compile(name: string, elem: Element, parentContext?: Context): CompiledTemplate {
|
_compile(name: string, elem: Element, parentContext?: Context): CompiledTemplate {
|
||||||
const isDebug = elem.attributes.hasOwnProperty("t-debug");
|
const isDebug = elem.attributes.hasOwnProperty("t-debug");
|
||||||
const ctx = new Context(name);
|
const ctx = new Context(name);
|
||||||
|
if (elem.tagName !== "t") {
|
||||||
|
ctx.shouldDefineResult = false;
|
||||||
|
}
|
||||||
if (parentContext) {
|
if (parentContext) {
|
||||||
|
ctx.templates = Object.create(parentContext.templates);
|
||||||
ctx.variables = Object.create(parentContext.variables);
|
ctx.variables = Object.create(parentContext.variables);
|
||||||
ctx.nextID = parentContext.parentNode! + 1;
|
ctx.nextID = parentContext.parentNode! + 1;
|
||||||
ctx.parentNode = parentContext.parentNode!;
|
ctx.parentNode = parentContext.parentNode!;
|
||||||
ctx.allowMultipleRoots = true;
|
ctx.allowMultipleRoots = true;
|
||||||
ctx.hasParentWidget = true;
|
ctx.hasParentWidget = true;
|
||||||
|
ctx.shouldDefineResult = false;
|
||||||
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
|
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
|
||||||
|
|
||||||
for (let v in parentContext.variables) {
|
for (let v in parentContext.variables) {
|
||||||
@@ -405,6 +421,7 @@ export class QWeb extends EventBus {
|
|||||||
// template rendering.
|
// template rendering.
|
||||||
let nodeID = ctx.generateID();
|
let nodeID = ctx.generateID();
|
||||||
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
|
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
|
||||||
|
ctx.addLine(`result = vn${nodeID};`);
|
||||||
ctx.rootContext.rootNode = nodeID;
|
ctx.rootContext.rootNode = nodeID;
|
||||||
ctx.rootContext.parentTextNode = nodeID;
|
ctx.rootContext.parentTextNode = nodeID;
|
||||||
}
|
}
|
||||||
@@ -479,6 +496,7 @@ export class QWeb extends EventBus {
|
|||||||
if (node.nodeName !== "t") {
|
if (node.nodeName !== "t") {
|
||||||
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
|
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
|
||||||
ctx = ctx.withParent(nodeID);
|
ctx = ctx.withParent(nodeID);
|
||||||
|
ctx = ctx.subContext("currentKey", ctx.lastNodeKey);
|
||||||
let nodeHooks = {};
|
let nodeHooks = {};
|
||||||
let addNodeHook = function(hook, handler) {
|
let addNodeHook = function(hook, handler) {
|
||||||
nodeHooks[hook] = nodeHooks[hook] || [];
|
nodeHooks[hook] = nodeHooks[hook] || [];
|
||||||
@@ -647,7 +665,9 @@ export class QWeb extends EventBus {
|
|||||||
let nodeID = ctx.generateID();
|
let nodeID = ctx.generateID();
|
||||||
let nodeKey: any = (<Element>node).getAttribute("t-key");
|
let nodeKey: any = (<Element>node).getAttribute("t-key");
|
||||||
if (nodeKey) {
|
if (nodeKey) {
|
||||||
nodeKey = ctx.formatExpression(nodeKey);
|
ctx.addLine(`const nodeKey${nodeID} = ${ctx.formatExpression(nodeKey)}`);
|
||||||
|
nodeKey = `nodeKey${nodeID}`;
|
||||||
|
ctx.lastNodeKey = nodeKey;
|
||||||
} else {
|
} else {
|
||||||
nodeKey = nodeID;
|
nodeKey = nodeID;
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-9
@@ -1,18 +1,18 @@
|
|||||||
import { Component } from "../component/component";
|
import { Component } from "../component/component";
|
||||||
|
import { xml } from "../tags";
|
||||||
import { Destination, RouterEnv } from "./Router";
|
import { Destination, RouterEnv } from "./Router";
|
||||||
|
|
||||||
export const LINK_TEMPLATE_NAME = "__owl__-router-link";
|
|
||||||
export const LINK_TEMPLATE = `
|
|
||||||
<a t-att-class="{'router-link-active': isActive }"
|
|
||||||
t-att-href="href"
|
|
||||||
t-on-click="navigate">
|
|
||||||
<t t-slot="default"/>
|
|
||||||
</a>`;
|
|
||||||
|
|
||||||
type Props = Destination;
|
type Props = Destination;
|
||||||
|
|
||||||
export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> {
|
export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> {
|
||||||
template = LINK_TEMPLATE_NAME;
|
static template = xml`
|
||||||
|
<a t-att-class="{'router-link-active': isActive }"
|
||||||
|
t-att-href="href"
|
||||||
|
t-on-click="navigate">
|
||||||
|
<t t-slot="default"/>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
|
||||||
href: string = this.env.router.destToPath(this.props);
|
href: string = this.env.router.destToPath(this.props);
|
||||||
|
|
||||||
async willUpdateProps(nextProps) {
|
async willUpdateProps(nextProps) {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Component } from "../component/component";
|
||||||
|
import { xml } from "../tags";
|
||||||
|
|
||||||
|
export class RouteComponent extends Component<any, {}, {}> {
|
||||||
|
static template = xml`
|
||||||
|
<t t-foreach="routes" t-as="route">
|
||||||
|
<t t-if="env.router.currentRouteName === route.name">
|
||||||
|
<t t-component="{{route.component}}" t-props="env.router.currentParams"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
`;
|
||||||
|
|
||||||
|
routes: any[] = [];
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
const router = this.env.router;
|
||||||
|
for (let name of router.routeIds) {
|
||||||
|
const route = router.routes[name];
|
||||||
|
if (route.component) {
|
||||||
|
this.routes.push({
|
||||||
|
name: route.name,
|
||||||
|
component: "__component__" + route.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
-15
@@ -1,7 +1,6 @@
|
|||||||
import { Env } from "../component/component";
|
import { Env } from "../component/component";
|
||||||
import { QWeb } from "../qweb/index";
|
import { QWeb } from "../qweb/index";
|
||||||
import { makeDirective } from "./directive";
|
import { shallowEqual } from "../utils";
|
||||||
import { LINK_TEMPLATE, LINK_TEMPLATE_NAME } from "./Link";
|
|
||||||
|
|
||||||
type NavigationGuard = (info: {
|
type NavigationGuard = (info: {
|
||||||
env: Env;
|
env: Env;
|
||||||
@@ -74,7 +73,7 @@ export class Router {
|
|||||||
partialRoute.name = "__route__" + nextId++;
|
partialRoute.name = "__route__" + nextId++;
|
||||||
}
|
}
|
||||||
if (partialRoute.component) {
|
if (partialRoute.component) {
|
||||||
QWeb.register("__component__" + partialRoute.name, partialRoute.component);
|
QWeb.registerComponent("__component__" + partialRoute.name, partialRoute.component);
|
||||||
}
|
}
|
||||||
if (partialRoute.redirect) {
|
if (partialRoute.redirect) {
|
||||||
this.validateDestination(partialRoute.redirect);
|
this.validateDestination(partialRoute.redirect);
|
||||||
@@ -83,13 +82,6 @@ export class Router {
|
|||||||
this.routes[partialRoute.name] = partialRoute as Route;
|
this.routes[partialRoute.name] = partialRoute as Route;
|
||||||
this.routeIds.push(partialRoute.name);
|
this.routeIds.push(partialRoute.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
(this as any)._listener = () => this.matchAndApplyRules(this.currentPath());
|
|
||||||
window.addEventListener("popstate", (this as any)._listener);
|
|
||||||
|
|
||||||
// setup link and directive
|
|
||||||
env.qweb.addTemplate(LINK_TEMPLATE_NAME, LINK_TEMPLATE);
|
|
||||||
QWeb.addDirective(makeDirective(<RouterEnv>env));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
@@ -97,6 +89,11 @@ export class Router {
|
|||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
|
(this as any)._listener = ev => this._navigate(this.currentPath(), ev);
|
||||||
|
window.addEventListener("popstate", (this as any)._listener);
|
||||||
|
if (this.mode === "hash") {
|
||||||
|
window.addEventListener("hashchange", (this as any)._listener);
|
||||||
|
}
|
||||||
const result = await this.matchAndApplyRules(this.currentPath());
|
const result = await this.matchAndApplyRules(this.currentPath());
|
||||||
if (result.type === "match") {
|
if (result.type === "match") {
|
||||||
this.currentRoute = result.route;
|
this.currentRoute = result.route;
|
||||||
@@ -110,18 +107,27 @@ export class Router {
|
|||||||
|
|
||||||
async navigate(to: Destination): Promise<boolean> {
|
async navigate(to: Destination): Promise<boolean> {
|
||||||
const path = this.destToPath(to);
|
const path = this.destToPath(to);
|
||||||
|
return this._navigate(path);
|
||||||
|
}
|
||||||
|
async _navigate(path: string, ev?: any): Promise<boolean> {
|
||||||
const initialName = this.currentRouteName;
|
const initialName = this.currentRouteName;
|
||||||
|
const initialParams = this.currentParams;
|
||||||
const result = await this.matchAndApplyRules(path);
|
const result = await this.matchAndApplyRules(path);
|
||||||
if (result.type === "match") {
|
if (result.type === "match") {
|
||||||
const finalPath = this.routeToPath(result.route, result.params);
|
const finalPath = this.routeToPath(result.route, result.params);
|
||||||
this.setUrlFromPath(finalPath);
|
const isPopStateEvent = ev && ev instanceof PopStateEvent;
|
||||||
|
if (!isPopStateEvent) {
|
||||||
|
this.setUrlFromPath(finalPath);
|
||||||
|
}
|
||||||
this.currentRoute = result.route;
|
this.currentRoute = result.route;
|
||||||
this.currentParams = result.params;
|
this.currentParams = result.params;
|
||||||
} else if (result.type === "nomatch") {
|
} else if (result.type === "nomatch") {
|
||||||
this.currentRoute = null;
|
this.currentRoute = null;
|
||||||
this.currentParams = null;
|
this.currentParams = null;
|
||||||
}
|
}
|
||||||
if (this.currentRouteName !== initialName) {
|
const didChange =
|
||||||
|
this.currentRouteName !== initialName || !shallowEqual(this.currentParams, initialParams);
|
||||||
|
if (didChange) {
|
||||||
this.env.qweb.forceUpdate();
|
this.env.qweb.forceUpdate();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -142,8 +148,11 @@ export class Router {
|
|||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
private setUrlFromPath(path: string) {
|
private setUrlFromPath(path: string) {
|
||||||
const url = location.origin + path;
|
const separator = this.mode === "hash" ? "/" : "";
|
||||||
window.history.pushState({}, path, url);
|
const url = location.origin + separator + path;
|
||||||
|
if (url !== window.location.href) {
|
||||||
|
window.history.pushState({}, path, url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private validateDestination(dest: Destination) {
|
private validateDestination(dest: Destination) {
|
||||||
@@ -164,7 +173,8 @@ export class Router {
|
|||||||
parts[i] = <string>params[key];
|
parts[i] = <string>params[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return parts.join("/");
|
const prefix = this.mode === "hash" ? "#" : "";
|
||||||
|
return prefix + parts.join("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
private currentPath(): string {
|
private currentPath(): string {
|
||||||
@@ -223,6 +233,9 @@ export class Router {
|
|||||||
if (route.path === "*") {
|
if (route.path === "*") {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
if (path.startsWith("#")) {
|
||||||
|
path = path.slice(1);
|
||||||
|
}
|
||||||
const descrParts = route.path.split("/");
|
const descrParts = route.path.split("/");
|
||||||
const targetParts = path.split("/");
|
const targetParts = path.split("/");
|
||||||
const l = descrParts.length;
|
const l = descrParts.length;
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { RouterEnv } from "./Router";
|
|
||||||
|
|
||||||
export function makeDirective(env: RouterEnv) {
|
|
||||||
return {
|
|
||||||
name: "routecomponent",
|
|
||||||
priority: 13,
|
|
||||||
atNodeEncounter({ node }): boolean {
|
|
||||||
let first = true;
|
|
||||||
const router = env.router;
|
|
||||||
for (let name of router.routeIds) {
|
|
||||||
const route = router.routes[name];
|
|
||||||
if (route.component) {
|
|
||||||
// make new t t-component element
|
|
||||||
const comp = node.ownerDocument.createElement("t");
|
|
||||||
comp.setAttribute("t-component", "__component__" + route.name);
|
|
||||||
comp.setAttribute(first ? "t-if" : "t-elif", `env.router.currentRouteName === '${route.name}'`);
|
|
||||||
first = false;
|
|
||||||
for (let param of route.params) {
|
|
||||||
comp.setAttribute(param, `env.router.currentParams.${param}`);
|
|
||||||
}
|
|
||||||
node.appendChild(comp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node.removeAttribute("t-routecomponent");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -56,6 +56,7 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
|
|||||||
const observer = store.observer;
|
const observer = store.observer;
|
||||||
const revFn = this.deep ? observer.deepRevNumber : observer.revNumber;
|
const revFn = this.deep ? observer.deepRevNumber : observer.revNumber;
|
||||||
(this.__owl__ as any).store = store;
|
(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).revFn = revFn.bind(observer);
|
||||||
(this.__owl__ as any).storeHash = this.hashFunction(this.storeProps, {
|
(this.__owl__ as any).storeHash = this.hashFunction(this.storeProps, {
|
||||||
prevStoreProps: this.storeProps
|
prevStoreProps: this.storeProps
|
||||||
@@ -81,13 +82,44 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
|
|||||||
super.__destroy(parent);
|
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, p, s, v) {
|
async __updateProps(nextProps: P, f, p, s, v) {
|
||||||
this.__updateStoreProps(nextProps);
|
this.__updateStoreProps(nextProps);
|
||||||
return super.__updateProps(nextProps, f, p, s, v);
|
return super.__updateProps(nextProps, f, p, s, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
__updateStoreProps(nextProps): boolean {
|
__updateStoreProps(nextProps): boolean {
|
||||||
const store = (this.__owl__ as any).store;
|
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(
|
const storeProps = (<any>this.constructor).mapStoreToProps(
|
||||||
store.state,
|
store.state,
|
||||||
nextProps,
|
nextProps,
|
||||||
@@ -97,23 +129,21 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
|
|||||||
const storeHash = this.hashFunction(storeProps, options);
|
const storeHash = this.hashFunction(storeProps, options);
|
||||||
this.storeProps = storeProps;
|
this.storeProps = storeProps;
|
||||||
let didChange = options.didChange;
|
let didChange = options.didChange;
|
||||||
if (storeHash !== (this.__owl__ as any).storeHash) {
|
if (storeHash !== __owl__.storeHash) {
|
||||||
(this.__owl__ as any).storeHash = storeHash;
|
__owl__.storeHash = storeHash;
|
||||||
didChange = true;
|
didChange = true;
|
||||||
}
|
}
|
||||||
(this.__owl__ as any).rev = store.observer.rev;
|
__owl__.rev = store.observer.rev;
|
||||||
|
__owl__.ownProps = nextProps;
|
||||||
return didChange;
|
return didChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
async __checkUpdate() {
|
async __checkUpdate() {
|
||||||
const observer = (this.__owl__ as any).store.observer;
|
|
||||||
if (observer.rev === (this.__owl__ as any).rev) {
|
|
||||||
// update was already done by updateProps, from parent
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const didChange = this.__updateStoreProps(this.props);
|
const didChange = this.__updateStoreProps(this.props);
|
||||||
if (didChange) {
|
if (didChange) {
|
||||||
return this.render();
|
return this.render();
|
||||||
}
|
}
|
||||||
|
// see note in render method
|
||||||
|
return (this.__owl__ as any).renderPromise;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -42,7 +42,7 @@ export class Store extends EventBus {
|
|||||||
debug: boolean;
|
debug: boolean;
|
||||||
env: any;
|
env: any;
|
||||||
observer: Observer;
|
observer: Observer;
|
||||||
getters: { [name: string]: (payload?) => any };
|
getters: { [name: string]: (payload?) => any };
|
||||||
|
|
||||||
constructor(config: StoreConfig, options: StoreOption = {}) {
|
constructor(config: StoreConfig, options: StoreOption = {}) {
|
||||||
super();
|
super();
|
||||||
@@ -54,13 +54,13 @@ export class Store extends EventBus {
|
|||||||
this.state = this.observer.observe(config.state || {});
|
this.state = this.observer.observe(config.state || {});
|
||||||
this.getters = {};
|
this.getters = {};
|
||||||
if (config.getters) {
|
if (config.getters) {
|
||||||
const firstArg = {
|
const firstArg = {
|
||||||
state: this.state,
|
state: this.state,
|
||||||
getters: this.getters,
|
getters: this.getters
|
||||||
};
|
};
|
||||||
for (let g in config.getters) {
|
for (let g in config.getters) {
|
||||||
this.getters[g] = config.getters[g].bind(this, firstArg);
|
this.getters[g] = config.getters[g].bind(this, firstArg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import { QWeb } from "./qweb/index";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owl Tags
|
||||||
|
*
|
||||||
|
* We have here a (very) small collection of tag functions:
|
||||||
|
*
|
||||||
|
* - xml
|
||||||
|
*
|
||||||
|
* The plan is to add a few other tags such as css, globalcss.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XML tag helper for defining templates. With this, one can simply define
|
||||||
|
* an inline template with just the template xml:
|
||||||
|
* ```js
|
||||||
|
* class A extends Component {
|
||||||
|
* static template = xml`<div>some template</div>`;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function xml(strings) {
|
||||||
|
const name = `__template__${QWeb.nextId++}`;
|
||||||
|
QWeb.registerTemplate(name, strings[0]);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
@@ -95,3 +95,12 @@ export function debounce(func: Function, wait: number, immediate?: boolean): Fun
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shallowEqual(p1, p2): boolean {
|
||||||
|
for (let k in p1) {
|
||||||
|
if (p1[k] !== p2[k]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -526,7 +526,7 @@ const htmlDomApi = {
|
|||||||
parentNode,
|
parentNode,
|
||||||
nextSibling,
|
nextSibling,
|
||||||
tagName,
|
tagName,
|
||||||
setTextContent,
|
setTextContent
|
||||||
} as DOMAPI;
|
} as DOMAPI;
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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;
|
||||||
@@ -26,12 +27,12 @@ exports[`animations t-transition combined with component 1`] = `
|
|||||||
}
|
}
|
||||||
if (!w4) {
|
if (!w4) {
|
||||||
let componentKey4 = \`Child\`;
|
let componentKey4 = \`Child\`;
|
||||||
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4];
|
||||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||||
w4 = new W4(parent, props4);
|
w4 = new W4(parent, props4);
|
||||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
def3 = w4.__prepare();
|
def3 = w4.__prepare();
|
||||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
||||||
w4.destroy();
|
w4.destroy();
|
||||||
};
|
};
|
||||||
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
@@ -54,6 +55,7 @@ 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;
|
||||||
@@ -71,12 +73,12 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
|
|||||||
}
|
}
|
||||||
if (!w4) {
|
if (!w4) {
|
||||||
let componentKey4 = \`Child\`;
|
let componentKey4 = \`Child\`;
|
||||||
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4];
|
||||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||||
w4 = new W4(parent, props4);
|
w4 = new W4(parent, props4);
|
||||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
def3 = w4.__prepare();
|
def3 = w4.__prepare();
|
||||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
||||||
w4.destroy();
|
w4.destroy();
|
||||||
};
|
};
|
||||||
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
@@ -97,6 +99,7 @@ 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');
|
||||||
@@ -117,6 +120,7 @@ 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');
|
||||||
|
|||||||
@@ -175,10 +175,10 @@ describe("animations", () => {
|
|||||||
|
|
||||||
env.qweb.addTemplate("Parent", `<div><Child t-transition="chimay"/></div>`);
|
env.qweb.addTemplate("Parent", `<div><Child t-transition="chimay"/></div>`);
|
||||||
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
||||||
class Parent extends Widget {
|
|
||||||
components = { Child: Child };
|
|
||||||
}
|
|
||||||
class Child extends Widget {}
|
class Child extends Widget {}
|
||||||
|
class Parent extends Widget {
|
||||||
|
static components = { Child: Child };
|
||||||
|
}
|
||||||
const widget = new Parent(env);
|
const widget = new Parent(env);
|
||||||
|
|
||||||
let def = makeDeferred();
|
let def = makeDeferred();
|
||||||
@@ -214,11 +214,11 @@ describe("animations", () => {
|
|||||||
`<div><t t-if="state.display" t-component="Child" t-transition="chimay"/></div>`
|
`<div><t t-if="state.display" t-component="Child" t-transition="chimay"/></div>`
|
||||||
);
|
);
|
||||||
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
||||||
|
class Child extends Widget {}
|
||||||
class Parent extends Widget {
|
class Parent extends Widget {
|
||||||
components = { Child: Child };
|
static components = { Child: Child };
|
||||||
state = { display: true };
|
state = { display: true };
|
||||||
}
|
}
|
||||||
class Child extends Widget {}
|
|
||||||
const widget = new Parent(env);
|
const widget = new Parent(env);
|
||||||
|
|
||||||
let def = makeDeferred();
|
let def = makeDeferred();
|
||||||
@@ -356,7 +356,7 @@ describe("animations", () => {
|
|||||||
);
|
);
|
||||||
class Child extends Widget {}
|
class Child extends Widget {}
|
||||||
class Parent extends Widget {
|
class Parent extends Widget {
|
||||||
components = { Child };
|
static components = { Child };
|
||||||
constructor(parent) {
|
constructor(parent) {
|
||||||
super(parent);
|
super(parent);
|
||||||
this.state = { flag: false };
|
this.state = { flag: false };
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ 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;
|
||||||
@@ -30,13 +31,13 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
|
|||||||
}
|
}
|
||||||
if (!w4) {
|
if (!w4) {
|
||||||
let componentKey4 = \`Child\`;
|
let componentKey4 = \`Child\`;
|
||||||
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4];
|
||||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||||
utils.validateProps(W4, props4)
|
utils.validateProps(W4, props4)
|
||||||
w4 = new W4(parent, props4);
|
w4 = new W4(parent, props4);
|
||||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
def3 = w4.__prepare();
|
def3 = w4.__prepare();
|
||||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
} else {
|
} else {
|
||||||
utils.validateProps(w4.constructor, props4)
|
utils.validateProps(w4.constructor, props4)
|
||||||
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||||
|
|||||||
+709
-432
File diff suppressed because it is too large
Load Diff
@@ -235,7 +235,7 @@ describe("props validation", () => {
|
|||||||
static props = ["message"];
|
static props = ["message"];
|
||||||
}
|
}
|
||||||
class App extends Widget {
|
class App extends Widget {
|
||||||
components = { Child };
|
static components = { Child };
|
||||||
}
|
}
|
||||||
const app = new App(env);
|
const app = new App(env);
|
||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
@@ -280,7 +280,7 @@ describe("props validation", () => {
|
|||||||
|
|
||||||
test("props: extra props cause an error, part 2", async () => {
|
test("props: extra props cause an error, part 2", async () => {
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
static props = {message: true};
|
static props = { message: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
@@ -294,23 +294,22 @@ describe("props validation", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
QWeb.utils.validateProps(TestWidget, { message: 1});
|
QWeb.utils.validateProps(TestWidget, { message: 1 });
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("optional prop do not cause an error if value is undefined", async () => {
|
test("optional prop do not cause an error if value is undefined", async () => {
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
static props = {message: {type: String, optional: true}};
|
static props = { message: { type: String, optional: true } };
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
QWeb.utils.validateProps(TestWidget, { message: undefined});
|
QWeb.utils.validateProps(TestWidget, { message: undefined });
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
expect(() => {
|
expect(() => {
|
||||||
QWeb.utils.validateProps(TestWidget, { message: null});
|
QWeb.utils.validateProps(TestWidget, { message: null });
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("default props", () => {
|
describe("default props", () => {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ describe("observer", () => {
|
|||||||
expect(observer.revNumber(obj)).toBe(1);
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(1);
|
expect(observer.deepRevNumber(obj)).toBe(1);
|
||||||
expect(observer.rev).toBe(1);
|
expect(observer.rev).toBe(1);
|
||||||
expect(typeof obj.date.getFullYear()).toBe('number');
|
expect(typeof obj.date.getFullYear()).toBe("number");
|
||||||
expect(obj.date).toBe(date);
|
expect(obj.date).toBe(date);
|
||||||
|
|
||||||
obj.date = new Date();
|
obj.date = new Date();
|
||||||
|
|||||||
+22
-1
@@ -5,6 +5,27 @@ import "../src/qweb/base_directives";
|
|||||||
import "../src/qweb/extensions";
|
import "../src/qweb/extensions";
|
||||||
import "../src/component/directive";
|
import "../src/component/directive";
|
||||||
|
|
||||||
|
// Some static cleanup
|
||||||
|
let nextSlotId;
|
||||||
|
let slots;
|
||||||
|
let nextId;
|
||||||
|
let TEMPLATES;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
nextSlotId = QWeb.nextSlotId;
|
||||||
|
slots = Object.assign({}, QWeb.slots);
|
||||||
|
nextId = QWeb.nextId;
|
||||||
|
TEMPLATES = Object.assign({}, QWeb.TEMPLATES);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
QWeb.nextSlotId = nextSlotId;
|
||||||
|
QWeb.slots = slots;
|
||||||
|
QWeb.nextId = nextId;
|
||||||
|
QWeb.TEMPLATES = TEMPLATES;
|
||||||
|
});
|
||||||
|
|
||||||
|
// helpers
|
||||||
export function nextMicroTick(): Promise<void> {
|
export function nextMicroTick(): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@@ -82,7 +103,7 @@ export function renderToString(qweb: QWeb, t: string, context: EvalContext = {})
|
|||||||
const node = renderToDOM(qweb, t, context);
|
const node = renderToDOM(qweb, t, context);
|
||||||
const result = node instanceof Text ? node.textContent! : node.outerHTML;
|
const result = node instanceof Text ? node.textContent! : node.outerHTML;
|
||||||
if (result !== qweb.renderToString(t, context)) {
|
if (result !== qweb.renderToString(t, context)) {
|
||||||
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
|
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ import { normalize, renderToDOM, renderToString, trim, nextTick } from "../helpe
|
|||||||
let qweb: QWeb;
|
let qweb: QWeb;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
QWeb.TEMPLATES = {};
|
||||||
qweb = new QWeb();
|
qweb = new QWeb();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -573,6 +574,94 @@ describe("t-call (template calling", () => {
|
|||||||
const expected = "<div>ok</div>";
|
const expected = "<div>ok</div>";
|
||||||
expect(trim(renderToString(qweb, "caller"))).toBe(expected);
|
expect(trim(renderToString(qweb, "caller"))).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("call with several sub nodes on same line", () => {
|
||||||
|
qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="SubTemplate">
|
||||||
|
<t t-raw="0"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="main">
|
||||||
|
<t t-call="SubTemplate">
|
||||||
|
<span>hey</span> <span>yay</span>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
const expected = "<div><div><span>hey</span> <span>yay</span></div></div>";
|
||||||
|
expect(renderToString(qweb, "main")).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("recursive template, part 1", () => {
|
||||||
|
qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="recursive">
|
||||||
|
<span>hey</span>
|
||||||
|
<t t-if="false">
|
||||||
|
<t t-call="recursive"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
const expected = "<div><span>hey</span></div>";
|
||||||
|
expect(renderToString(qweb, "recursive")).toBe(expected);
|
||||||
|
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
||||||
|
expect(recursiveFn.toString()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("recursive template, part 2", () => {
|
||||||
|
qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="Parent">
|
||||||
|
<t t-call="nodeTemplate">
|
||||||
|
<t t-set="node" t-value="root"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<div t-name="nodeTemplate">
|
||||||
|
<p><t t-esc="node.val"/></p>
|
||||||
|
<t t-foreach="node.children or []" t-as="subtree">
|
||||||
|
<t t-call="nodeTemplate">
|
||||||
|
<t t-set="node" t-value="subtree"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
const root = { val: "a", children: [{ val: "b" }, { val: "c" }] };
|
||||||
|
const expected = "<div><div><p>a</p><div><p>b</p></div><div><p>c</p></div></div></div>";
|
||||||
|
expect(renderToString(qweb, "Parent", { root })).toBe(expected);
|
||||||
|
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
||||||
|
expect(recursiveFn.toString()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("recursive template, part 3", () => {
|
||||||
|
qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="Parent">
|
||||||
|
<t t-call="nodeTemplate">
|
||||||
|
<t t-set="node" t-value="root"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<div t-name="nodeTemplate">
|
||||||
|
<p><t t-esc="node.val"/></p>
|
||||||
|
<t t-foreach="node.children or []" t-as="subtree">
|
||||||
|
<t t-call="nodeTemplate">
|
||||||
|
<t t-set="node" t-value="subtree"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
const root = { val: "a", children: [{ val: "b", children: [{ val: "d" }] }, { val: "c" }] };
|
||||||
|
const expected =
|
||||||
|
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>";
|
||||||
|
expect(renderToString(qweb, "Parent", { root })).toBe(expected);
|
||||||
|
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
||||||
|
expect(recursiveFn.toString()).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("foreach", () => {
|
describe("foreach", () => {
|
||||||
@@ -1190,3 +1279,22 @@ describe("update on event bus", () => {
|
|||||||
expect(fn).toBeCalledTimes(1);
|
expect(fn).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("global template registration", () => {
|
||||||
|
test("can register template globally", () => {
|
||||||
|
expect.assertions(5);
|
||||||
|
let qweb = new QWeb();
|
||||||
|
try {
|
||||||
|
qweb.render("mytemplate");
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toMatch("Template mytemplate does not exist");
|
||||||
|
}
|
||||||
|
expect(qweb.templates.mytemplate).toBeUndefined();
|
||||||
|
|
||||||
|
QWeb.registerTemplate("mytemplate", "<div>global</div>");
|
||||||
|
expect(qweb.templates.mytemplate).toBeDefined();
|
||||||
|
const vnode = qweb.render("mytemplate");
|
||||||
|
expect(vnode.sel).toBe("div");
|
||||||
|
expect((vnode as any).children[0].text).toBe("global");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component } from "../../src/component/component";
|
import { Component } from "../../src/component/component";
|
||||||
import { Link, LINK_TEMPLATE_NAME } 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 "./TestRouter";
|
||||||
@@ -31,7 +31,7 @@ describe("Link component", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any, any> {
|
||||||
components = { Link: Link };
|
static components = { Link: Link };
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
|
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
|
||||||
@@ -50,7 +50,7 @@ describe("Link component", () => {
|
|||||||
'<div><a href="/about" class="router-link-active">About</a></div>'
|
'<div><a href="/about" class="router-link-active">About</a></div>'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(env.qweb.templates[LINK_TEMPLATE_NAME].fn.toString()).toMatchSnapshot();
|
expect(env.qweb.templates[Link.template].fn.toString()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("do not redirect if right clicking", async () => {
|
test("do not redirect if right clicking", async () => {
|
||||||
@@ -62,7 +62,7 @@ describe("Link component", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any, any> {
|
||||||
components = { Link: Link };
|
static components = { Link: Link };
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
|
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
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 { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
||||||
import { TestRouter } from "./TestRouter";
|
import { TestRouter } from "./TestRouter";
|
||||||
|
|
||||||
describe("router directive t-routecomponent", () => {
|
describe("RouteComponent", () => {
|
||||||
let fixture: HTMLElement;
|
let fixture: HTMLElement;
|
||||||
let env: RouterEnv;
|
let env: RouterEnv;
|
||||||
let router: TestRouter | null = null;
|
let router: TestRouter | null = null;
|
||||||
@@ -25,7 +26,7 @@ describe("router directive t-routecomponent", () => {
|
|||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<div t-name="App">
|
<div t-name="App">
|
||||||
<t t-routecomponent="1" />
|
<RouteComponent />
|
||||||
</div>
|
</div>
|
||||||
<span t-name="About">About</span>
|
<span t-name="About">About</span>
|
||||||
<span t-name="Users">Users</span>
|
<span t-name="Users">Users</span>
|
||||||
@@ -34,7 +35,7 @@ describe("router directive t-routecomponent", () => {
|
|||||||
class About extends Component<any, any, any> {}
|
class About extends Component<any, any, any> {}
|
||||||
class Users extends Component<any, any, any> {}
|
class Users extends Component<any, any, any> {}
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any, any> {
|
||||||
components = { About, Users };
|
static components = { RouteComponent };
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -51,21 +52,21 @@ describe("router directive t-routecomponent", () => {
|
|||||||
await router.navigate({ to: "users" });
|
await router.navigate({ to: "users" });
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<div><span>Users</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>Users</span></div>");
|
||||||
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
|
expect(env.qweb.templates[RouteComponent.template].fn.toString()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can render parameterized route", async () => {
|
test("can render parameterized route", async () => {
|
||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<div t-name="App">
|
<div t-name="App">
|
||||||
<t t-routecomponent="1" />
|
<RouteComponent />
|
||||||
</div>
|
</div>
|
||||||
<span t-name="Book">Book <t t-esc="props.title"/></span>
|
<span t-name="Book">Book <t t-esc="props.title"/></span>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class Book extends Component<any, any, any> {}
|
class Book extends Component<any, any, any> {}
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any, any> {
|
||||||
components = { Book };
|
static components = { RouteComponent };
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = [{ name: "book", path: "/book/{{title}}", component: Book }];
|
const routes = [{ name: "book", path: "/book/{{title}}", component: Book }];
|
||||||
@@ -74,14 +75,13 @@ describe("router directive t-routecomponent", () => {
|
|||||||
const app = new App(env);
|
const app = new App(env);
|
||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div><span>Book 1984</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>Book 1984</span></div>");
|
||||||
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can render parameterized route with suffixes", async () => {
|
test("can render parameterized route with suffixes", async () => {
|
||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
<div t-name="App">
|
<div t-name="App">
|
||||||
<t t-routecomponent="1" />
|
<RouteComponent />
|
||||||
</div>
|
</div>
|
||||||
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
|
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
|
||||||
</templates>
|
</templates>
|
||||||
@@ -92,7 +92,7 @@ describe("router directive t-routecomponent", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any, any> {
|
||||||
components = { Book };
|
static components = { RouteComponent };
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = [{ name: "book", path: "/book/{{title}}/{{val.number}}", component: Book }];
|
const routes = [{ name: "book", path: "/book/{{title}}/{{val.number}}", component: Book }];
|
||||||
@@ -101,6 +101,5 @@ describe("router directive t-routecomponent", () => {
|
|||||||
const app = new App(env);
|
const app = new App(env);
|
||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>");
|
||||||
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -4,8 +4,6 @@ import { QWeb } from "../../src/qweb/index";
|
|||||||
export class TestRouter extends Router {
|
export class TestRouter extends Router {
|
||||||
destroy() {
|
destroy() {
|
||||||
window.removeEventListener("popstate", (this as any)._listener);
|
window.removeEventListener("popstate", (this as any)._listener);
|
||||||
delete QWeb.DIRECTIVE_NAMES.routecomponent;
|
|
||||||
QWeb.DIRECTIVES = QWeb.DIRECTIVES.filter(d => d.name !== "routecomponent");
|
|
||||||
|
|
||||||
// remove component defined inroutes
|
// remove component defined inroutes
|
||||||
for (let key in QWeb.components) {
|
for (let key in QWeb.components) {
|
||||||
|
|||||||
@@ -10,14 +10,15 @@ 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 '__owl__-router-link'\`)
|
throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__template__1'\`)
|
||||||
}
|
}
|
||||||
extra.handlers['click' + 3] = extra.handlers['click' + 3] || context['navigate'].bind(owner);
|
extra.handlers['click' + 3] = extra.handlers['click' + 3] || context['navigate'].bind(owner);
|
||||||
p3.on['click'] = extra.handlers['click' + 3];
|
p3.on['click'] = extra.handlers['click' + 3];
|
||||||
const slot4 = this.slots[context.__owl__.slotId + '_' + 'default'];
|
const slot4 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
|
||||||
if (slot4) {
|
if (slot4) {
|
||||||
slot4(context.__owl__.parent, Object.assign({}, extra, {parentNode: c3, vars: extra.vars, parent: owner}));
|
slot4.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c3, vars: extra.vars, parent: owner}));
|
||||||
}
|
}
|
||||||
return vn3;
|
return vn3;
|
||||||
}"
|
}"
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`RouteComponent can render simple cases 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
let QWeb = this.constructor;
|
||||||
|
let parent = context;
|
||||||
|
let owner = context;
|
||||||
|
let result;
|
||||||
|
context = Object.create(context);
|
||||||
|
var h = this.h;
|
||||||
|
var _1 = context['routes'];
|
||||||
|
if (!_1) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
|
var _2 = _3 = _1;
|
||||||
|
if (!(_1 instanceof Array)) {
|
||||||
|
_2 = Object.keys(_1);
|
||||||
|
_3 = Object.values(_1);
|
||||||
|
}
|
||||||
|
var _length2 = _2.length;
|
||||||
|
for (let i = 0; i < _length2; i++) {
|
||||||
|
context.route_first = i === 0;
|
||||||
|
context.route_last = i === _length2 - 1;
|
||||||
|
context.route_index = i;
|
||||||
|
context.route = _2[i];
|
||||||
|
context.route_value = _3[i];
|
||||||
|
if (context['env'].router.currentRouteName===context['route'].name) {
|
||||||
|
//COMPONENT
|
||||||
|
let def5;
|
||||||
|
let templateId7 = String(-6 - i);
|
||||||
|
let w6 = templateId7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId7]] : false;
|
||||||
|
let vn8 = {};
|
||||||
|
result = vn8;
|
||||||
|
let props6 = Object.assign({}, context['env'].router.currentParams);
|
||||||
|
if (w6 && w6.__owl__.renderPromise && !w6.__owl__.vnode) {
|
||||||
|
if (utils.shallowEqual(props6, w6.__owl__.renderProps)) {
|
||||||
|
def5 = w6.__owl__.renderPromise;
|
||||||
|
} else {
|
||||||
|
w6.destroy();
|
||||||
|
w6 = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!w6) {
|
||||||
|
let componentKey6 = (context['route'].component);
|
||||||
|
let W6 = context.constructor.components[componentKey6] || QWeb.components[componentKey6];
|
||||||
|
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
|
||||||
|
w6 = new W6(parent, props6);
|
||||||
|
parent.__owl__.cmap[templateId7] = w6.__owl__.id;
|
||||||
|
def5 = w6.__prepare();
|
||||||
|
def5 = def5.then(vnode=>{if (w6.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId7, hook: {insert(vn) {let nvn=w6.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w6.destroy();}}});utils.defineProxy(vn8, pvnode);w6.__owl__.pvnode = pvnode;});
|
||||||
|
} else {
|
||||||
|
def5 = def5 || w6.__updateProps(props6, extra.forceUpdate, extra.patchQueue);
|
||||||
|
def5 = def5.then(()=>{if (w6.__owl__.isDestroyed) {return};let pvnode=w6.__owl__.pvnode;utils.defineProxy(vn8, pvnode);});
|
||||||
|
}
|
||||||
|
extra.promises.push(def5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`router directive t-routecomponent can render parameterized route 1`] = `
|
|
||||||
"function anonymous(context,extra
|
|
||||||
) {
|
|
||||||
let utils = this.constructor.utils;
|
|
||||||
let QWeb = this.constructor;
|
|
||||||
let parent = context;
|
|
||||||
let owner = context;
|
|
||||||
var h = this.h;
|
|
||||||
let c1 = [], p1 = {key:1};
|
|
||||||
var vn1 = h('div', p1, c1);
|
|
||||||
if (context['env'].router.currentRouteName==='book') {
|
|
||||||
//COMPONENT
|
|
||||||
let def3;
|
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
|
||||||
let _2_index = c1.length;
|
|
||||||
c1.push(null);
|
|
||||||
let props4 = {title:context['env'].router.currentParams.title};
|
|
||||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
|
||||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
|
||||||
def3 = w4.__owl__.renderPromise;
|
|
||||||
} else {
|
|
||||||
w4.destroy();
|
|
||||||
w4 = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!w4) {
|
|
||||||
let componentKey4 = \`__component__book\`;
|
|
||||||
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
|
||||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
|
||||||
w4 = new W4(parent, props4);
|
|
||||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
|
||||||
def3 = w4.__prepare();
|
|
||||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
|
||||||
} else {
|
|
||||||
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
|
||||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
|
||||||
}
|
|
||||||
extra.promises.push(def3);
|
|
||||||
}
|
|
||||||
return vn1;
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`router directive t-routecomponent can render parameterized route with suffixes 1`] = `
|
|
||||||
"function anonymous(context,extra
|
|
||||||
) {
|
|
||||||
let utils = this.constructor.utils;
|
|
||||||
let QWeb = this.constructor;
|
|
||||||
let parent = context;
|
|
||||||
let owner = context;
|
|
||||||
var h = this.h;
|
|
||||||
let c1 = [], p1 = {key:1};
|
|
||||||
var vn1 = h('div', p1, c1);
|
|
||||||
if (context['env'].router.currentRouteName==='book') {
|
|
||||||
//COMPONENT
|
|
||||||
let def3;
|
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
|
||||||
let _2_index = c1.length;
|
|
||||||
c1.push(null);
|
|
||||||
let props4 = {title:context['env'].router.currentParams.title,val:context['env'].router.currentParams.val};
|
|
||||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
|
||||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
|
||||||
def3 = w4.__owl__.renderPromise;
|
|
||||||
} else {
|
|
||||||
w4.destroy();
|
|
||||||
w4 = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!w4) {
|
|
||||||
let componentKey4 = \`__component__book\`;
|
|
||||||
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
|
||||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
|
||||||
w4 = new W4(parent, props4);
|
|
||||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
|
||||||
def3 = w4.__prepare();
|
|
||||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
|
||||||
} else {
|
|
||||||
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
|
||||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
|
||||||
}
|
|
||||||
extra.promises.push(def3);
|
|
||||||
}
|
|
||||||
return vn1;
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`router directive t-routecomponent can render simple cases 1`] = `
|
|
||||||
"function anonymous(context,extra
|
|
||||||
) {
|
|
||||||
let utils = this.constructor.utils;
|
|
||||||
let QWeb = this.constructor;
|
|
||||||
let parent = context;
|
|
||||||
let owner = context;
|
|
||||||
var h = this.h;
|
|
||||||
let c1 = [], p1 = {key:1};
|
|
||||||
var vn1 = h('div', p1, c1);
|
|
||||||
if (context['env'].router.currentRouteName==='about') {
|
|
||||||
//COMPONENT
|
|
||||||
let def3;
|
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
|
||||||
let _2_index = c1.length;
|
|
||||||
c1.push(null);
|
|
||||||
let props4 = {};
|
|
||||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
|
||||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
|
||||||
def3 = w4.__owl__.renderPromise;
|
|
||||||
} else {
|
|
||||||
w4.destroy();
|
|
||||||
w4 = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!w4) {
|
|
||||||
let componentKey4 = \`__component__about\`;
|
|
||||||
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
|
||||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
|
||||||
w4 = new W4(parent, props4);
|
|
||||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
|
||||||
def3 = w4.__prepare();
|
|
||||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
|
||||||
} else {
|
|
||||||
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
|
||||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
|
||||||
}
|
|
||||||
extra.promises.push(def3);
|
|
||||||
}
|
|
||||||
else if (context['env'].router.currentRouteName==='users') {
|
|
||||||
//COMPONENT
|
|
||||||
let def6;
|
|
||||||
let w7 = 7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[7]] : false;
|
|
||||||
let _5_index = c1.length;
|
|
||||||
c1.push(null);
|
|
||||||
let props7 = {};
|
|
||||||
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
|
|
||||||
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
|
|
||||||
def6 = w7.__owl__.renderPromise;
|
|
||||||
} else {
|
|
||||||
w7.destroy();
|
|
||||||
w7 = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!w7) {
|
|
||||||
let componentKey7 = \`__component__users\`;
|
|
||||||
let W7 = context.components && context.components[componentKey7] || QWeb.components[componentKey7];
|
|
||||||
if (!W7) {throw new Error('Cannot find the definition of component \\"' + componentKey7 + '\\"')}
|
|
||||||
w7 = new W7(parent, props7);
|
|
||||||
parent.__owl__.cmap[7] = w7.__owl__.id;
|
|
||||||
def6 = w7.__prepare();
|
|
||||||
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: 7, hook: {insert(vn) {let nvn=w7.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
|
|
||||||
} else {
|
|
||||||
def6 = def6 || w7.__updateProps(props7, extra.forceUpdate, extra.patchQueue);
|
|
||||||
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c1[_5_index]=pvnode;});
|
|
||||||
}
|
|
||||||
extra.promises.push(def6);
|
|
||||||
}
|
|
||||||
return vn1;
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
+103
-25
@@ -1,5 +1,5 @@
|
|||||||
import { Destination, Router, RouterEnv, Route } from "../../src/router/Router";
|
import { Destination, RouterEnv, Route } from "../../src/router/Router";
|
||||||
import { makeTestEnv } from "../helpers";
|
import { makeTestEnv, nextTick } from "../helpers";
|
||||||
import { TestRouter } from "./TestRouter";
|
import { TestRouter } from "./TestRouter";
|
||||||
|
|
||||||
let env: RouterEnv;
|
let env: RouterEnv;
|
||||||
@@ -25,18 +25,58 @@ describe("router miscellaneous", () => {
|
|||||||
]);
|
]);
|
||||||
}).toThrow(`Invalid destination: {"abc":"hey"}`);
|
}).toThrow(`Invalid destination: {"abc":"hey"}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("navigate to same route but with different params should trigger update", async () => {
|
||||||
|
router = new TestRouter(env, [{ name: "users", path: "/users/{{id}}" }]);
|
||||||
|
env.qweb.forceUpdate = jest.fn();
|
||||||
|
await router.navigate({ to: "users", params: { id: 3 } });
|
||||||
|
expect(window.location.pathname).toBe("/users/3");
|
||||||
|
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await router.navigate({ to: "users", params: { id: 5 } });
|
||||||
|
expect(window.location.pathname).toBe("/users/5");
|
||||||
|
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("changing url to same route but with different params should trigger update (hash mode)", async () => {
|
||||||
|
env.qweb.forceUpdate = jest.fn();
|
||||||
|
router = new TestRouter(env, [{ name: "users", path: "/users/{{id}}" }], { mode: "hash" });
|
||||||
|
await router.start();
|
||||||
|
await router.navigate({ to: "users", params: { id: 3 } });
|
||||||
|
expect(window.location.hash).toBe("#/users/3");
|
||||||
|
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
window.location.hash = "/users/5";
|
||||||
|
window.dispatchEvent(new Event("hashchange"));
|
||||||
|
await nextTick();
|
||||||
|
expect(window.location.hash).toBe("#/users/5");
|
||||||
|
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("routeToPath", () => {
|
describe("routeToPath", () => {
|
||||||
const routeToPath = Router.prototype["routeToPath"];
|
|
||||||
test("simple non parameterized path", () => {
|
test("simple non parameterized path", () => {
|
||||||
expect(routeToPath({path: "/abc"} as Route, {})).toBe("/abc");
|
router = new TestRouter(env, []);
|
||||||
expect(routeToPath({path: "/abc/def"} as Route, {})).toBe("/abc/def");
|
expect(router["routeToPath"]({ path: "/abc" } as Route, {})).toBe("/abc");
|
||||||
expect(routeToPath({path: "/abc"} as Route, { val: 12 })).toBe("/abc");
|
expect(router["routeToPath"]({ path: "/abc/def" } as Route, {})).toBe("/abc/def");
|
||||||
|
expect(router["routeToPath"]({ path: "/abc" } as Route, { val: 12 })).toBe("/abc");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("simple parameterized path", () => {
|
test("simple parameterized path", () => {
|
||||||
expect(routeToPath({path: "/abc/{{def}}"} as Route, { def: 34 })).toBe("/abc/34");
|
router = new TestRouter(env, []);
|
||||||
|
expect(router["routeToPath"]({ path: "/abc/{{def}}" } as Route, { def: 34 })).toBe("/abc/34");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simple non parameterized path, mode = hash", () => {
|
||||||
|
router = new TestRouter(env, [], { mode: "hash" });
|
||||||
|
expect(router["routeToPath"]({ path: "/abc" } as Route, {})).toBe("#/abc");
|
||||||
|
expect(router["routeToPath"]({ path: "/abc/def" } as Route, {})).toBe("#/abc/def");
|
||||||
|
expect(router["routeToPath"]({ path: "/abc" } as Route, { val: 12 })).toBe("#/abc");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simple parameterized path, mode=hash", () => {
|
||||||
|
router = new TestRouter(env, [], { mode: "hash" });
|
||||||
|
expect(router["routeToPath"]({ path: "/abc/{{def}}" } as Route, { def: 34 })).toBe("#/abc/34");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,26 +98,60 @@ describe("destToPath", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("getRouteParams", () => {
|
describe("getRouteParams", () => {
|
||||||
const getRouteParams = Router.prototype["getRouteParams"];
|
|
||||||
test("properly match simple routes", () => {
|
test("properly match simple routes", () => {
|
||||||
|
router = new TestRouter(env, []);
|
||||||
// simple route
|
// simple route
|
||||||
expect(getRouteParams({path: "/home"} as Route, "/home")).toEqual({});
|
expect(router["getRouteParams"]({ path: "/home" } as Route, "/home")).toEqual({});
|
||||||
|
|
||||||
// no match
|
// no match
|
||||||
expect(getRouteParams({path: "/home"} as Route, "/otherpath")).toEqual(false);
|
expect(router["getRouteParams"]({ path: "/home" } as Route, "/otherpath")).toEqual(false);
|
||||||
|
|
||||||
// fallback route
|
// fallback route
|
||||||
expect(getRouteParams({path: "*"} as Route, "somepath")).toEqual({});
|
expect(router["getRouteParams"]({ path: "*" } as Route, "somepath")).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("properly match simple routes, mode hash", () => {
|
||||||
|
router = new TestRouter(env, [], { mode: "hash" });
|
||||||
|
// simple route
|
||||||
|
expect(router["getRouteParams"]({ path: "/home" } as Route, "#/home")).toEqual({});
|
||||||
|
|
||||||
|
// no match
|
||||||
|
expect(router["getRouteParams"]({ path: "/home" } as Route, "#/otherpath")).toEqual(false);
|
||||||
|
|
||||||
|
// fallback route
|
||||||
|
expect(router["getRouteParams"]({ path: "*" } as Route, "#/somepath")).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("match some parameterized routes", () => {
|
test("match some parameterized routes", () => {
|
||||||
expect(getRouteParams({path: "/invoices/{{id}}"} as Route, "/invoices/3")).toEqual({
|
router = new TestRouter(env, []);
|
||||||
|
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "/invoices/3")).toEqual({
|
||||||
id: "3"
|
id: "3"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("match some parameterized routes, mode hash", () => {
|
||||||
|
router = new TestRouter(env, [], { mode: "hash" });
|
||||||
|
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "#/invoices/3")).toEqual(
|
||||||
|
{
|
||||||
|
id: "3"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("can convert to number if needed", () => {
|
test("can convert to number if needed", () => {
|
||||||
expect(getRouteParams({path: "/invoices/{{id.number}}"} as Route, "/invoices/3")).toEqual({
|
router = new TestRouter(env, []);
|
||||||
|
expect(
|
||||||
|
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "/invoices/3")
|
||||||
|
).toEqual({
|
||||||
|
id: 3
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can convert to number if needed, mode: hash", () => {
|
||||||
|
router = new TestRouter(env, [], { mode: "hash" });
|
||||||
|
expect(
|
||||||
|
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "#/invoices/3")
|
||||||
|
).toEqual({
|
||||||
id: 3
|
id: 3
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -131,9 +205,9 @@ describe("beforeRouteEnter", () => {
|
|||||||
expect(window.location.pathname).toBe("/");
|
expect(window.location.pathname).toBe("/");
|
||||||
const guard = jest.fn(() => false);
|
const guard = jest.fn(() => false);
|
||||||
router = new TestRouter(env, [
|
router = new TestRouter(env, [
|
||||||
{ name: "routea", path: "/some/patha"},
|
{ name: "routea", path: "/some/patha" },
|
||||||
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard }
|
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await router.start();
|
await router.start();
|
||||||
await router.navigate({ to: "routea" });
|
await router.navigate({ to: "routea" });
|
||||||
@@ -147,12 +221,14 @@ describe("beforeRouteEnter", () => {
|
|||||||
|
|
||||||
test("navigation is redirected if guard decides so", async () => {
|
test("navigation is redirected if guard decides so", async () => {
|
||||||
expect(window.location.pathname).toBe("/");
|
expect(window.location.pathname).toBe("/");
|
||||||
const guard = jest.fn(() => {return {to: "routec"}});
|
const guard = jest.fn(() => {
|
||||||
|
return { to: "routec" };
|
||||||
|
});
|
||||||
router = new TestRouter(env, [
|
router = new TestRouter(env, [
|
||||||
{ name: "routea", path: "/some/patha"},
|
{ name: "routea", path: "/some/patha" },
|
||||||
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard },
|
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard },
|
||||||
{ name: "routec", path: "/some/pathc"},
|
{ name: "routec", path: "/some/pathc" }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await router.start();
|
await router.start();
|
||||||
const result = await router.navigate({ to: "routea" });
|
const result = await router.navigate({ to: "routea" });
|
||||||
@@ -166,11 +242,13 @@ describe("beforeRouteEnter", () => {
|
|||||||
|
|
||||||
test("navigation is initially redirected if guard decides so", async () => {
|
test("navigation is initially redirected if guard decides so", async () => {
|
||||||
expect(window.location.pathname).toBe("/");
|
expect(window.location.pathname).toBe("/");
|
||||||
const guard = jest.fn(() => {return {to: "otherroute"}});
|
const guard = jest.fn(() => {
|
||||||
|
return { to: "otherroute" };
|
||||||
|
});
|
||||||
router = new TestRouter(env, [
|
router = new TestRouter(env, [
|
||||||
{ name: "landing", path: "/", beforeRouteEnter: guard},
|
{ name: "landing", path: "/", beforeRouteEnter: guard },
|
||||||
{ name: "otherroute", path: "/some/other/route"}
|
{ name: "otherroute", path: "/some/other/route" }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(window.location.pathname).toBe("/");
|
expect(window.location.pathname).toBe("/");
|
||||||
|
|
||||||
|
|||||||
@@ -15,3 +15,7 @@ exports[`connecting a component to store deep and shallow connecting a component
|
|||||||
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 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[`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>"`;
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ describe("connecting a component to store", () => {
|
|||||||
<span t-name="Todo"><t t-esc="props.msg"/></span>
|
<span t-name="Todo"><t t-esc="props.msg"/></span>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
class Todo extends Component<any, any, any> {}
|
||||||
class App extends ConnectedComponent<any, any, any> {
|
class App extends ConnectedComponent<any, any, any> {
|
||||||
components = { Todo };
|
static components = { Todo };
|
||||||
static mapStoreToProps(s) {
|
static mapStoreToProps(s) {
|
||||||
return { todos: s.todos };
|
return { todos: s.todos };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class Todo extends Component<any, any, any> {}
|
|
||||||
const state = { todos: [] };
|
const state = { todos: [] };
|
||||||
const actions = {
|
const actions = {
|
||||||
addTodo({ state }, msg) {
|
addTodo({ state }, msg) {
|
||||||
@@ -122,7 +122,7 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
class App extends ConnectedComponent<any, any, any> {
|
class App extends ConnectedComponent<any, any, any> {
|
||||||
components = { Todo };
|
static components = { Todo };
|
||||||
static mapStoreToProps(s) {
|
static mapStoreToProps(s) {
|
||||||
return { todos: s.todos };
|
return { todos: s.todos };
|
||||||
}
|
}
|
||||||
@@ -200,7 +200,7 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Parent extends Component<any, any, any> {
|
class Parent extends Component<any, any, any> {
|
||||||
components = { Child };
|
static components = { Child };
|
||||||
|
|
||||||
constructor(env: Env) {
|
constructor(env: Env) {
|
||||||
super(env);
|
super(env);
|
||||||
@@ -248,7 +248,7 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TodoList extends ConnectedComponent<any, any, any> {
|
class TodoList extends ConnectedComponent<any, any, any> {
|
||||||
components = { TodoItem };
|
static components = { TodoItem };
|
||||||
static mapStoreToProps(state) {
|
static mapStoreToProps(state) {
|
||||||
return { todos: state.todos };
|
return { todos: state.todos };
|
||||||
}
|
}
|
||||||
@@ -305,7 +305,7 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TodoList extends ConnectedComponent<any, any, any> {
|
class TodoList extends ConnectedComponent<any, any, any> {
|
||||||
components = { TodoItem };
|
static components = { TodoItem };
|
||||||
static mapStoreToProps(state) {
|
static mapStoreToProps(state) {
|
||||||
return { todos: state.todos };
|
return { todos: state.todos };
|
||||||
}
|
}
|
||||||
@@ -337,7 +337,7 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any, any> {
|
||||||
components = { Beer };
|
static components = { Beer };
|
||||||
state = { beerId: 1 };
|
state = { beerId: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +414,7 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any, any> {
|
||||||
components = { Beer };
|
static components = { Beer };
|
||||||
state = { beerId: 0 };
|
state = { beerId: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +481,7 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any, any> {
|
||||||
components = { Beer };
|
static components = { Beer };
|
||||||
state = { beerId: 0 };
|
state = { beerId: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,20 +559,19 @@ describe("connecting a component to store", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class Parent extends ConnectedComponent<any, any, any> {
|
|
||||||
components = { Child };
|
|
||||||
static mapStoreToProps(s) {
|
|
||||||
steps.push("parent");
|
|
||||||
return { current: s.current, isvisible: s.isvisible };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Child extends ConnectedComponent<any, any, any> {
|
class Child extends ConnectedComponent<any, any, any> {
|
||||||
static mapStoreToProps(s, props) {
|
static mapStoreToProps(s, props) {
|
||||||
steps.push("child");
|
steps.push("child");
|
||||||
return { msg: s.msg[props.key] };
|
return { msg: s.msg[props.key] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class Parent extends ConnectedComponent<any, any, any> {
|
||||||
|
static components = { Child };
|
||||||
|
static mapStoreToProps(s) {
|
||||||
|
steps.push("parent");
|
||||||
|
return { current: s.current, isvisible: s.isvisible };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const state = { current: "a", msg: { a: "a", b: "b" } };
|
const state = { current: "a", msg: { a: "a", b: "b" } };
|
||||||
const actions = {
|
const actions = {
|
||||||
@@ -609,26 +608,25 @@ describe("connecting a component to store", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class Parent extends ConnectedComponent<any, any, any> {
|
|
||||||
components = { Child };
|
|
||||||
static mapStoreToProps(s) {
|
|
||||||
steps.push("parent");
|
|
||||||
return { flag: s.flag, someId: s.someId };
|
|
||||||
}
|
|
||||||
async render(force) {
|
|
||||||
await def;
|
|
||||||
return super.render(force);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Child extends ConnectedComponent<any, any, any> {
|
class Child extends ConnectedComponent<any, any, any> {
|
||||||
static mapStoreToProps(s, props) {
|
static mapStoreToProps(s, props) {
|
||||||
steps.push("child");
|
steps.push("child");
|
||||||
return { msg: s.messages[props.someId] };
|
return { msg: s.messages[props.someId] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class Parent extends ConnectedComponent<any, any, any> {
|
||||||
|
static components = { Child };
|
||||||
|
static mapStoreToProps(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 state = { someId: 1, flag: true, messages: { 1: "abc" } };
|
||||||
const actions = {
|
const actions = {
|
||||||
setFlagToFalse({ state }) {
|
setFlagToFalse({ state }) {
|
||||||
state.flag = false;
|
state.flag = false;
|
||||||
@@ -685,15 +683,6 @@ describe("connecting a component to store", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class TodoApp extends ConnectedComponent<any, any, any> {
|
|
||||||
components = { TodoItem };
|
|
||||||
static mapStoreToProps(state) {
|
|
||||||
return {
|
|
||||||
todos: state.todos
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let renderCount = 0;
|
let renderCount = 0;
|
||||||
let fCount = 0;
|
let fCount = 0;
|
||||||
|
|
||||||
@@ -714,6 +703,14 @@ describe("connecting a component to store", () => {
|
|||||||
return super.__render(...args);
|
return super.__render(...args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class TodoApp extends ConnectedComponent<any, any, any> {
|
||||||
|
static components = { TodoItem };
|
||||||
|
static mapStoreToProps(state) {
|
||||||
|
return {
|
||||||
|
todos: state.todos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(<any>env).store = store;
|
(<any>env).store = store;
|
||||||
const app = new TodoApp(env);
|
const app = new TodoApp(env);
|
||||||
@@ -764,15 +761,6 @@ describe("connecting a component to store", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class TodoApp extends ConnectedComponent<any, any, any> {
|
|
||||||
components = { TodoItem };
|
|
||||||
static mapStoreToProps(state) {
|
|
||||||
return {
|
|
||||||
todos: state.todos
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let renderCount = 0;
|
let renderCount = 0;
|
||||||
let fCount = 0;
|
let fCount = 0;
|
||||||
|
|
||||||
@@ -794,6 +782,15 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TodoApp extends ConnectedComponent<any, any, any> {
|
||||||
|
static components = { TodoItem };
|
||||||
|
static mapStoreToProps(state) {
|
||||||
|
return {
|
||||||
|
todos: state.todos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(<any>env).store = store;
|
(<any>env).store = store;
|
||||||
const app = new TodoApp(env);
|
const app = new TodoApp(env);
|
||||||
|
|
||||||
@@ -879,7 +876,7 @@ describe("connected components and default values", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any, any> {
|
||||||
components = { Greeter };
|
static components = { Greeter };
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = new Store({ state: {} });
|
const store = new Store({ state: {} });
|
||||||
@@ -903,7 +900,7 @@ describe("connected components and default values", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any, any> {
|
||||||
components = { Greeter };
|
static components = { Greeter };
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = new Store({ state: {} });
|
const store = new Store({ state: {} });
|
||||||
@@ -948,7 +945,7 @@ describe("connected components and default values", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Thread extends ConnectedComponent<any, any, any> {
|
class Thread extends ConnectedComponent<any, any, any> {
|
||||||
components = { Message };
|
static components = { Message };
|
||||||
static defaultProps = { showMessages: true };
|
static defaultProps = { showMessages: true };
|
||||||
static mapStoreToProps = function(state, ownProps) {
|
static mapStoreToProps = function(state, ownProps) {
|
||||||
const thread = state.threads[ownProps.threadId];
|
const thread = state.threads[ownProps.threadId];
|
||||||
@@ -959,7 +956,7 @@ describe("connected components and default values", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any, any> {
|
||||||
components = { Thread };
|
static components = { Thread };
|
||||||
static defaultProps = { threadId: 1 };
|
static defaultProps = { threadId: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1026,7 +1023,7 @@ describe("connected components and default values", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Parent extends Component<any, any, any> {
|
class Parent extends Component<any, any, any> {
|
||||||
components = { Child };
|
static components = { Child };
|
||||||
state = { child: true };
|
state = { child: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1040,7 +1037,7 @@ describe("connected components and default values", () => {
|
|||||||
super.off(eventType, owner);
|
super.off(eventType, owner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const store = new TestStore({ state: {val: 1} });
|
const store = new TestStore({ state: { val: 1 } });
|
||||||
(<any>env).store = store;
|
(<any>env).store = store;
|
||||||
const parent = new Parent(env);
|
const parent = new Parent(env);
|
||||||
|
|
||||||
@@ -1088,9 +1085,91 @@ describe("connected components and default values", () => {
|
|||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div>0</div>");
|
expect(fixture.innerHTML).toBe("<div>0</div>");
|
||||||
|
|
||||||
const res = app.dispatch('inc');
|
const res = app.dispatch("inc");
|
||||||
expect(res).toBe(1);
|
expect(res).toBe(1);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<div>1</div>");
|
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 });
|
||||||
|
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="Message">
|
||||||
|
<button t-on-click="doStuff">Do stuff</button>
|
||||||
|
<Attachment t-foreach="storeProps.attachmentIds" t-key="attachmentId" t-as="attachmentId" id="attachmentId"/>
|
||||||
|
</div>
|
||||||
|
<div t-name="Attachment">
|
||||||
|
<span>Attachment <t t-esc="props.id"/></span>
|
||||||
|
<span>Name: <t t-esc="storeProps.name"/></span>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
class Attachment extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps(state, ownProps) {
|
||||||
|
return {
|
||||||
|
name: state.attachments[ownProps.id].name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Message extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps(state) {
|
||||||
|
return {
|
||||||
|
attachmentIds: state.messages[10].attachmentIds
|
||||||
|
};
|
||||||
|
}
|
||||||
|
static components = { Attachment };
|
||||||
|
state = { isAttachmentDeleted: false };
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+24
-24
@@ -249,32 +249,32 @@ describe("advanced state properties", () => {
|
|||||||
expect(store.state.a).toEqual([1, 2, 3, 53]);
|
expect(store.state.a).toEqual([1, 2, 3, 53]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can use object assign in store", async () => {
|
test("can use object assign in store", async () => {
|
||||||
const actions = {
|
const actions = {
|
||||||
dosomething({ state }) {
|
dosomething({ state }) {
|
||||||
Object.assign(state.westmalle, { a: 3, b: 4 });
|
Object.assign(state.westmalle, { a: 3, b: 4 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const store = new Store({
|
const store = new Store({
|
||||||
state: { westmalle: { a: 1, b: 2 } },
|
state: { westmalle: { a: 1, b: 2 } },
|
||||||
actions
|
actions
|
||||||
});
|
|
||||||
store.dispatch("dosomething");
|
|
||||||
expect(store.state.westmalle).toEqual({ a: 3, b: 4 });
|
|
||||||
});
|
});
|
||||||
|
store.dispatch("dosomething");
|
||||||
|
expect(store.state.westmalle).toEqual({ a: 3, b: 4 });
|
||||||
|
});
|
||||||
|
|
||||||
test("aku reactive store state 1", async () => {
|
test("aku reactive store state 1", async () => {
|
||||||
const actions = {
|
const actions = {
|
||||||
inc({ state }) {
|
inc({ state }) {
|
||||||
state.counter++;
|
state.counter++;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const state = { counter: 0 };
|
const state = { counter: 0 };
|
||||||
const store = new Store({ state, actions });
|
const store = new Store({ state, actions });
|
||||||
expect(store.state.counter).toBe(0);
|
expect(store.state.counter).toBe(0);
|
||||||
store.dispatch("inc", {});
|
store.dispatch("inc", {});
|
||||||
expect(store.state.counter).toBe(1);
|
expect(store.state.counter).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("updates triggered by the store", () => {
|
describe("updates triggered by the store", () => {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class Counter extends owl.Component {
|
|||||||
// Message Widget
|
// Message Widget
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
class Message extends owl.Component {
|
class Message extends owl.Component {
|
||||||
components = { Counter };
|
static components = { Counter };
|
||||||
|
|
||||||
shouldUpdate(nextProps) {
|
shouldUpdate(nextProps) {
|
||||||
return nextProps.message !== this.props.message;
|
return nextProps.message !== this.props.message;
|
||||||
@@ -31,7 +31,7 @@ class Message extends owl.Component {
|
|||||||
// Root Widget
|
// Root Widget
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
components = { Message };
|
static components = { Message };
|
||||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
+92
-91
@@ -150,6 +150,97 @@ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start);
|
|||||||
return zip.generateAsync({ type: "blob" });
|
return zip.generateAsync({ type: "blob" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Tabbed editor
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class TabbedEditor extends owl.Component {
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
this.state = {
|
||||||
|
currentTab: props.js ? "js" : props.xml ? "xml" : "css"
|
||||||
|
};
|
||||||
|
this.setTab = owl.utils.debounce(this.setTab, 250, true);
|
||||||
|
|
||||||
|
this.sessions = {};
|
||||||
|
this._setupSessions(props);
|
||||||
|
this.editor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.editor = this.editor || ace.edit(this.refs.editor);
|
||||||
|
|
||||||
|
this.editor.setValue(this.props[this.state.currentTab], -1);
|
||||||
|
this.editor.setFontSize("12px");
|
||||||
|
this.editor.setTheme("ace/theme/monokai");
|
||||||
|
this.editor.setSession(this.sessions[this.state.currentTab]);
|
||||||
|
const tabSize = this.state.currentTab === "xml" ? 2 : 4;
|
||||||
|
this.editor.session.setOption("tabSize", tabSize);
|
||||||
|
this.editor.on("blur", () => {
|
||||||
|
const editorValue = this.editor.getValue();
|
||||||
|
const propsValue = this.props[this.state.currentTab];
|
||||||
|
if (editorValue !== propsValue) {
|
||||||
|
this.trigger("updateCode", {
|
||||||
|
type: this.state.currentTab,
|
||||||
|
value: editorValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
willUpdateProps(nextProps) {
|
||||||
|
this._setupSessions(nextProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
patched() {
|
||||||
|
const session = this.sessions[this.state.currentTab];
|
||||||
|
let content = this.props[this.state.currentTab];
|
||||||
|
if (content === false) {
|
||||||
|
const tab = this.props.js ? "js" : this.props.xml ? "xml" : "css";
|
||||||
|
content = this.props[tab];
|
||||||
|
this.state.currentTab = tab;
|
||||||
|
}
|
||||||
|
session.setValue(content, -1);
|
||||||
|
this.editor.setSession(session);
|
||||||
|
this.editor.resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTab(tab) {
|
||||||
|
if (this.state.currentTab !== tab) {
|
||||||
|
this.state.currentTab = tab;
|
||||||
|
const session = this.sessions[this.state.currentTab];
|
||||||
|
session.doc.setValue(this.props[tab], -1);
|
||||||
|
this.editor.setSession(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseDown(ev) {
|
||||||
|
if (ev.target.tagName === "DIV") {
|
||||||
|
let y = ev.clientY;
|
||||||
|
const resizer = ev => {
|
||||||
|
const delta = ev.clientY - y;
|
||||||
|
y = ev.clientY;
|
||||||
|
this.trigger("updatePanelHeight", { delta });
|
||||||
|
};
|
||||||
|
document.body.addEventListener("mousemove", resizer);
|
||||||
|
document.body.addEventListener("mouseup", () => {
|
||||||
|
document.body.removeEventListener("mousemove", resizer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupSessions(props) {
|
||||||
|
for (let tab of ["js", "xml", "css"]) {
|
||||||
|
if (props[tab] && !this.sessions[tab]) {
|
||||||
|
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
|
||||||
|
this.sessions[tab].setOption("useWorker", false);
|
||||||
|
const tabSize = tab === "xml" ? 2 : 4;
|
||||||
|
this.sessions[tab].setOption("tabSize", tabSize);
|
||||||
|
this.sessions[tab].setUndoManager(new ace.UndoManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// MAIN APP
|
// MAIN APP
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -158,7 +249,6 @@ class App extends owl.Component {
|
|||||||
super(...args);
|
super(...args);
|
||||||
this.version = owl.__info__.version;
|
this.version = owl.__info__.version;
|
||||||
this.SAMPLES = SAMPLES;
|
this.SAMPLES = SAMPLES;
|
||||||
this.components = { TabbedEditor };
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
js: SAMPLES[0].code,
|
js: SAMPLES[0].code,
|
||||||
@@ -270,97 +360,8 @@ class App extends owl.Component {
|
|||||||
saveAs(content, "app.zip");
|
saveAs(content, "app.zip");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
App.components = { TabbedEditor };
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Tabbed editor
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
class TabbedEditor extends owl.Component {
|
|
||||||
constructor(parent, props) {
|
|
||||||
super(parent, props);
|
|
||||||
this.state = {
|
|
||||||
currentTab: props.js ? "js" : props.xml ? "xml" : "css"
|
|
||||||
};
|
|
||||||
this.setTab = owl.utils.debounce(this.setTab, 250, true);
|
|
||||||
|
|
||||||
this.sessions = {};
|
|
||||||
this._setupSessions(props);
|
|
||||||
this.editor = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.editor = this.editor || ace.edit(this.refs.editor);
|
|
||||||
|
|
||||||
this.editor.setValue(this.props[this.state.currentTab], -1);
|
|
||||||
this.editor.setFontSize("12px");
|
|
||||||
this.editor.setTheme("ace/theme/monokai");
|
|
||||||
this.editor.setSession(this.sessions[this.state.currentTab]);
|
|
||||||
const tabSize = this.state.currentTab === "xml" ? 2 : 4;
|
|
||||||
this.editor.session.setOption("tabSize", tabSize);
|
|
||||||
this.editor.on("blur", () => {
|
|
||||||
const editorValue = this.editor.getValue();
|
|
||||||
const propsValue = this.props[this.state.currentTab];
|
|
||||||
if (editorValue !== propsValue) {
|
|
||||||
this.trigger("updateCode", {
|
|
||||||
type: this.state.currentTab,
|
|
||||||
value: editorValue
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
willUpdateProps(nextProps) {
|
|
||||||
this._setupSessions(nextProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
patched() {
|
|
||||||
const session = this.sessions[this.state.currentTab];
|
|
||||||
let content = this.props[this.state.currentTab];
|
|
||||||
if (content === false) {
|
|
||||||
const tab = this.props.js ? "js" : this.props.xml ? "xml" : "css";
|
|
||||||
content = this.props[tab];
|
|
||||||
this.state.currentTab = tab;
|
|
||||||
}
|
|
||||||
session.setValue(content, -1);
|
|
||||||
this.editor.setSession(session);
|
|
||||||
this.editor.resize();
|
|
||||||
}
|
|
||||||
|
|
||||||
setTab(tab) {
|
|
||||||
if (this.state.currentTab !== tab) {
|
|
||||||
this.state.currentTab = tab;
|
|
||||||
const session = this.sessions[this.state.currentTab];
|
|
||||||
session.doc.setValue(this.props[tab], -1);
|
|
||||||
this.editor.setSession(session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseDown(ev) {
|
|
||||||
if (ev.target.tagName === "DIV") {
|
|
||||||
let y = ev.clientY;
|
|
||||||
const resizer = ev => {
|
|
||||||
const delta = ev.clientY - y;
|
|
||||||
y = ev.clientY;
|
|
||||||
this.trigger("updatePanelHeight", { delta });
|
|
||||||
};
|
|
||||||
document.body.addEventListener("mousemove", resizer);
|
|
||||||
document.body.addEventListener("mouseup", () => {
|
|
||||||
document.body.removeEventListener("mousemove", resizer);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_setupSessions(props) {
|
|
||||||
for (let tab of ["js", "xml", "css"]) {
|
|
||||||
if (props[tab] && !this.sessions[tab]) {
|
|
||||||
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
|
|
||||||
this.sessions[tab].setOption("useWorker", false);
|
|
||||||
const tabSize = tab === "xml" ? 2 : 4;
|
|
||||||
this.sessions[tab].setOption("tabSize", tabSize);
|
|
||||||
this.sessions[tab].setUndoManager(new ace.UndoManager());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Application initialization
|
// Application initialization
|
||||||
|
|||||||
+28
-29
@@ -10,7 +10,7 @@ class Greeter extends owl.Component {
|
|||||||
|
|
||||||
// Main root component
|
// Main root component
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
components = { Greeter };
|
static components = { Greeter };
|
||||||
state = { name: 'World'};
|
state = { name: 'World'};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ class Counter extends owl.Component {
|
|||||||
|
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
state = { flag: false, componentFlag: false, numbers: [] };
|
state = { flag: false, componentFlag: false, numbers: [] };
|
||||||
components = { Counter };
|
static components = { Counter };
|
||||||
|
|
||||||
toggle(key) {
|
toggle(key) {
|
||||||
this.state[key] = !this.state[key];
|
this.state[key] = !this.state[key];
|
||||||
@@ -214,7 +214,7 @@ class DemoComponent extends owl.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
components = { DemoComponent };
|
static components = { DemoComponent };
|
||||||
state = { n: 0, flag: true };
|
state = { n: 0, flag: true };
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
@@ -388,7 +388,7 @@ class TodoItem extends owl.Component {
|
|||||||
// TodoApp
|
// TodoApp
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
class TodoApp extends owl.store.ConnectedComponent {
|
class TodoApp extends owl.store.ConnectedComponent {
|
||||||
components = { TodoItem };
|
static components = { TodoItem };
|
||||||
state = { filter: "all" };
|
state = { filter: "all" };
|
||||||
|
|
||||||
static mapStoreToProps(state) {
|
static mapStoreToProps(state) {
|
||||||
@@ -896,25 +896,24 @@ const RESPONSIVE = `// In this example, we show how we can modify keys in the gl
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
class Navbar extends owl.Component {}
|
class Navbar extends owl.Component {}
|
||||||
|
|
||||||
class ControlPanel extends owl.Component {
|
class MobileSearchView extends owl.Component {}
|
||||||
components = { MobileSearchView };
|
|
||||||
}
|
|
||||||
|
|
||||||
class FormView extends owl.Component {
|
class ControlPanel extends owl.Component {
|
||||||
components = { AdvancedComponent };
|
static components = { MobileSearchView };
|
||||||
}
|
}
|
||||||
|
|
||||||
class AdvancedComponent extends owl.Component {}
|
class AdvancedComponent extends owl.Component {}
|
||||||
|
|
||||||
|
class FormView extends owl.Component {
|
||||||
|
static components = { AdvancedComponent };
|
||||||
|
}
|
||||||
|
|
||||||
class Chatter extends owl.Component {
|
class Chatter extends owl.Component {
|
||||||
messages = Array.from(Array(100).keys());
|
messages = Array.from(Array(100).keys());
|
||||||
}
|
}
|
||||||
|
|
||||||
class MobileSearchView extends owl.Component {}
|
|
||||||
|
|
||||||
|
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
components = { Navbar, ControlPanel, FormView, Chatter };
|
static components = { Navbar, ControlPanel, FormView, Chatter };
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -1072,7 +1071,7 @@ class Counter extends owl.Component {
|
|||||||
|
|
||||||
// Main root component
|
// Main root component
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
components = {Card, Counter};
|
static components = {Card, Counter};
|
||||||
state = {a: 1, b: 3};
|
state = {a: 1, b: 3};
|
||||||
|
|
||||||
inc(key, delta) {
|
inc(key, delta) {
|
||||||
@@ -1170,8 +1169,18 @@ const ASYNC_COMPONENTS = `// This example will not work if your browser does not
|
|||||||
// because of the slow component. We use the 't-asyncroot' directive for this
|
// because of the slow component. We use the 't-asyncroot' directive for this
|
||||||
// purpose. Try removing it to see the difference.
|
// purpose. Try removing it to see the difference.
|
||||||
|
|
||||||
|
class SlowComponent extends owl.Component {
|
||||||
|
willUpdateProps() {
|
||||||
|
// simulate a component that needs to perform async stuff (e.g. an RPC)
|
||||||
|
// with the updated props before re-rendering itself
|
||||||
|
return new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationList extends owl.Component {}
|
||||||
|
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
components = {SlowComponent, NotificationList};
|
static components = {SlowComponent, NotificationList};
|
||||||
state = { value: 0, notifs: [] };
|
state = { value: 0, notifs: [] };
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
@@ -1185,16 +1194,6 @@ class App extends owl.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SlowComponent extends owl.Component {
|
|
||||||
willUpdateProps() {
|
|
||||||
// simulate a component that needs to perform async stuff (e.g. an RPC)
|
|
||||||
// with the updated props before re-rendering itself
|
|
||||||
return new Promise(resolve => setTimeout(resolve, 1500));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NotificationList extends owl.Component {}
|
|
||||||
|
|
||||||
const qweb = new owl.QWeb(TEMPLATES);
|
const qweb = new owl.QWeb(TEMPLATES);
|
||||||
const app = new App({ qweb });
|
const app = new App({ qweb });
|
||||||
app.mount(document.body);
|
app.mount(document.body);
|
||||||
@@ -1368,7 +1367,7 @@ class Window extends owl.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WindowManager extends owl.Component {
|
class WindowManager extends owl.Component {
|
||||||
components = { Window };
|
static components = { Window };
|
||||||
windows = [];
|
windows = [];
|
||||||
nextId = 1;
|
nextId = 1;
|
||||||
currentZindex = 1;
|
currentZindex = 1;
|
||||||
@@ -1384,12 +1383,12 @@ class WindowManager extends owl.Component {
|
|||||||
left: 0,
|
left: 0,
|
||||||
zindex: this.currentZindex++
|
zindex: this.currentZindex++
|
||||||
});
|
});
|
||||||
this.components[id] = info.component;
|
this.constructor.components[id] = info.component;
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
closeWindow(ev) {
|
closeWindow(ev) {
|
||||||
const id = ev.detail.id;
|
const id = ev.detail.id;
|
||||||
delete this.components[id];
|
delete this.constructor.components[id];
|
||||||
const index = this.windows.findIndex(w => w.id === id);
|
const index = this.windows.findIndex(w => w.id === id);
|
||||||
this.windows.splice(index, 1);
|
this.windows.splice(index, 1);
|
||||||
this.render();
|
this.render();
|
||||||
@@ -1409,7 +1408,7 @@ class WindowManager extends owl.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
components = { WindowManager };
|
static components = { WindowManager };
|
||||||
|
|
||||||
addWindow(name) {
|
addWindow(name) {
|
||||||
this.refs.wm.addWindow(name);
|
this.refs.wm.addWindow(name);
|
||||||
|
|||||||
Reference in New Issue
Block a user