Compare commits

..

33 Commits

Author SHA1 Message Date
Simon Genin (ges) 6aaa174a8a wip 2020-10-14 20:33:00 +02:00
Simon Genin (ges) 35121dff59 wip 2020-10-14 15:16:22 +02:00
Simon Genin 6a833c54ec [IMP] setup github actions for CI
Add action to launch the tests on the PRs
2020-10-13 16:57:24 +02:00
Géry Debongnie cb38d795f9 [FIX] slots: properly handle named t-slots inside named t-slots
Previous code naively handled nested t-set-slots: if a second named
slots was found, it overrode the first.

In this commit, we use a set to make sure that we only use the first
found t-set-slot node. Also, we ignore set-slots defined in a sub
components, because these slots are only relevant to the sub component
itself.

Note that it works as expected because document.querySelectorAll
performs a search depth first, so we will always use the named slots
closer to the parent element, in term of depth.

closes #682
2020-10-09 15:01:25 +02:00
Géry Debongnie 8da68e925b [REL] v1.0.11
# Owl v1.0.11

- improve release script to make sure we get correct code on npm
2020-10-08 10:50:39 +02:00
Géry Debongnie 474ffa1cd9 [FIX] release: try to publish correct version on npm 2020-10-08 10:48:08 +02:00
Géry Debongnie e73fb462c5 [REL] v1.0.10
# v1.0.10

Bug fixes!

- qweb fix: scoping issue with t-call in t-foreach
- qweb fix: issue with t-set with a body in a t-call
- qweb fix: handle input value attribute as a property
- qweb fix: allow t-call on arbitrary nodes
- qweb fix: add indeterminate to special input properties
- component fix: allow using t-model with bracketed expression
- component fix: properly validate multiple props
- component fix: issue with higher order component, and t-keys
- component fix: issue with unmounted children that should be destroyed
- component fix: make concurrent renderings more robust in some cases
- component fix: allow using vars with body as props
- observer fix: do not proxify promises
- test infrastructure: stop mocking requestanimation frame
2020-09-18 15:16:27 +02:00
Géry Debongnie 38c7ad9629 [FIX] t-model: support expressions with [ ]
Before this commit, the t-model directive worked well with expressions
such as "state.value", but not with bracketed expression: "state[value]"
(it generated invalid code).

This commit make the t-model smarter by detecting this case, and
properly capturing the base expression and key variable.

closes #694
2020-09-18 14:58:48 +02:00
Géry Debongnie d0c76c5854 [IMP] qweb: add indeterminate to special input properties
Input with type="checkbox" have a special property (indeterminate) to
visually display the fact that the input value is non determinate (in my
chrome browser, the checkbox is then drawn with a simple - inside). It
does not actually modify the value of the input, only the way it is
displayed.

So, with this commit, owl will properly set the property, as expected.

closes #713
2020-09-18 13:43:25 +02:00
Géry Debongnie e032314739 [FIX] qweb: allow t-call on arbitrary html nodes
This is a rarely (if ever) used feature, but according to our qweb
reference implementation, it is possible to use the
t-call directive on an arbitrary html tag, like this:

<div t-call="my.template"/>

It is then interpreted as:

<div><t t-call="my.template"/></div>

So, with this commit, we make sure that the owl qweb implementation
matches that behaviour.

closes #706
2020-09-18 08:39:57 +02:00
Géry Debongnie b2db7f21ed [FIX] observer: does not proxify promises
Promises are kind of special, and do not behave like usual javascript
values.

For this issue, the problem is that when the observer tries to observe a
promise value, the code will crash with an error like this:

Uncaught TypeError: Method Promise.prototype.then called on incompatible receiver [object Object]

Also, note that it does not make much sense to proxify promise methods
anyway, since they are not (supposed) to be modified.

So, with this commit, we simply consider that promises should be treated
like a primitive value: simply ignored when determining if it should be
proxified

Note that I actually believe that putting promises in a useState is not
a good idea in general.

closes #677
2020-09-17 13:51:10 +02:00
Géry Debongnie 7b8ac13d3f [REF] qweb: mostly revert fix with t-call and vars with body
The previous fix (overriding tostring of VDomArray) is actually more
general, and solves the same issue. So, let us simplify the code and
keep the more general solution.

This reverts commit 3bf91afc3f.
2020-09-17 09:44:08 +02:00
Géry Debongnie 06d852fcf9 [FIX] component: allow using vars with body as props
Consider this scenario:

- a variable v (with a body) is defined in a template
- it is then passed to a sub component as a prop
- and now, it is t-esc-ed.

Before this commit, the displayed value was [object object], because the
value actually passed to the sub component was a VDomArray (internal
structure used to represent nodelists)

This issue is actually quite a problem in practice, because values in a
templates are translated, but not in attributes.  Therefore, using a
t-set directive with a body text content is the proper way to have
translated values at runtime.

We override in this commit the method toString of VDomArray to make sure
it is properly displayed.

Note that we considered changing the way props were generated (by trying
to detect VDomArray, then calling vDomToString), but then the value
would not be able to be used in a t-raw.  Also, it is quite elegant to
be able to format the VDomArray only at the end.

closes #670
2020-09-17 09:44:08 +02:00
Géry Debongnie 7e4baf668a [FIX] test: timing issue
Before this commit, we artificially replaced in the tests the
requestAnimationFrame by a setTimeout, to actually increase the speed of
the tests.  However, this is not really a true replacement.  For
example, a real setTimeout can come before or after a real
nextAnimationFrame, depending on when/where it is requested.

Also, this change exposed another problem: the nextTick function did a
setTimeout before a nextanimationframe. This is not a problem when
nextAnimationFrame is replaced by a setTimeout, because then all
expectations holds in owl.  However, it is wrong: to get to the next
animation frame, we need to request an animation frame, and THEN wait
with a setTimeout.

closes #729
2020-09-17 09:18:05 +02:00
Géry Debongnie fe34ba00a6 [FIX] component: properly validate multiple props
Because of a "break" statement instead of "continue", the check for valid
props was stopping as soon as it met an optional props, which kind of
invalidate the whole system.

closes #717
2020-09-17 08:33:18 +02:00
Géry Debongnie e5e7790530 [FIX] qweb: handle input value attribute as a property
Sometimes, HTML is slightly more subtle than what I initially expect.
Rendering some html is simple, we have tags and attributes.  However,
once we add behaviour, then the situation is more complex:

<input value="abc"/>

is an input with an INITIAL value of "abc", but the attribute does not
actually represent the CURRENT value of the input, which may be
different if the user did change it.

This is basically the difference between "attribute" and "property".

So, when rendering html with owl, we sometimes want to actually set
the property (current value), instead of the html attribute.

This commit make sure that this is the case for inputs with the "value"
attribute.

closes #722
2020-09-17 08:32:02 +02:00
Géry Debongnie 3bf91afc3f [FIX] qweb: fix issue variables set in body of t-call
The body of a t-call directive may be used to define private variables
to the sub template call.

However, the code that handles t-call worked like this:

- compile sub template if necessary
- then compile body of t-call to extract variables

This means that the variables defined in the t-call body were not yet
processed and available in the context.  Because of that, when the call
to t-esc is done, there is not internal qweb var, and the code simply
outputs a scope['varname'], which is in our case a VDOMArray, so it is
displayed as [object object]

What this fix does is changing the way t-esc works: if we are in the
context of a sub template, then it assumes that any outside variable may
or may not be a VDomArray, so it needs to check and eventually convert
it to a string, if necessary.

closes #719
2020-09-16 11:39:32 +02:00
Géry Debongnie 2529aa3ef2 [FIX] component: make concurrent renderings more robust
Here is a situation that can happen in some complicated case:

1. a parent component is rendered, which includes some children
2. it is then willPatched
3. the sub components are then mounted/willUnmounted
4. because of complicated business logic, this causes the parent
component to be rerendered (before parent "patched" method is called)
5. owl will internally reset its currentfiber to null (but there is a
pending rendering!)
6. subsequent rendering will ignore pending rendering
7. havoc ensues

This is actually one of the reason why modifying a component state in a
willPatch component is actually not a good idea.  However, the good news
is that this specific situation can be properly handled: we can simply
make sure that we do not reset currentFiber to null if there is a new
pending rendering.

closes #728
2020-09-16 09:21:33 +02:00
Géry Debongnie 8d25bddda4 [FIX] component: subtle issue with unmounted children
Owl has to manage a lot of interesting situations.  One of them is when
a rendering is initiated, which creates a sub component, but then
another rendering starts, which invalidate the previous one, and will
create another sub component.  Since the first sub component was not
ever in the DOM, we cannot rely on the vdom patching process to remove
it, so we have to do it manually.

Sadly, this is actually a very tricky situation, since there are other
subtle situations where the code that remove an unmounted widget could
be executed, in particular when the parent component is unmounted, then
remounted, then modified to trigger yet another rendering.

In this commit, we handle this case more carefully by making sure that
the destroyed subcomponent properly configures its pvnode so the patch
process happens as expected.

joint work with the framework team, and in particular LPE for his work on
finding a testcase!

closes #724, #731
2020-09-15 15:51:30 +02:00
Lucas Perais (lpe) 81af21a025 [FIX] component, fiber: update props with virtual node should not crash
Have a hierarchy of A, B, C components where:

```xml
<div t-name="A">
  <div>
    <B t-key="key1"/>
  </div>
</div>

<t t-name="B">
  <C t-key="key2"/>
</t>

<div t-name="C">
  <div><t t-esc="keys_as_props" /></div>
</div>
```

The subtility of the issues lies in B, which doesn't have its own
concrete DOM element, rather, it borrows it from C.

With the sequence of events:
- change key2
C1 is destroyed and replaced by another instance, and another node.
B1 has its props updated and is patched with the C2's node (CRITICAL)
A1 is patched

- change key1 AND key2
C2 is destroyed
B1 is destroyed
A1 is patched replacing B1 by B2, and their nodes too (which at this point should be C2's to C3's)

Before this commit, at the CRITICAL point, the node representing the component itself
(technically its pvnode) was not updated with the new concrete node provided by B1 patch with C2 node
i.e. it held the previous node still
The second array of steps crashed because at A1 patch, the new B2 node would replace B1, which
was out of the DOM (removed because C1 was destroyed long before),
and therefore without a viable parent to insert B2 node.

After this commit, we update the component's pvnode after the patch which elm had possibly changed
There is no crash anymore for this use case.
2020-09-14 15:17:45 +02:00
Jigar Patel 9baea2c1cd [DOC] props validation: The validation type should be a String object. 2020-07-28 08:56:33 +02:00
Lucas Perais (lpe) 8e03f9cd9c [FIX] qweb: t-call should protect scope and let it accessible
Have a t-call nested in a t-foreach nested in a t-foreach

```xml
<t t-name="template">
  <t t-foreach="..." t-as="a">
    <t t-foreach="..." t-as="b">
      <t-call="templateCalled" />
    </t>
  </t>
</t>
```

Before this commit, the `a` variable was not accessible within the t-call.
That was because the way t-call protected its scope by hiding other protected scope
in this case, the first protected scope for the first `t-foreach` was hidden

After this commit, `a` and `b` are accessible in the t-call, whether the t-call
defines its own variables by `t-set` or not.
Also, as expected from other fixes, there is no leaks of variables defined within a `t-call`

fixes #695
2020-07-28 08:56:00 +02:00
Géry Debongnie c5a2f52afb [REL] v1.0.9
# v1.0.9

- qweb fix: add support for svg namespace in t-raw
- qweb fix: properly handle subtemplates in shared templates
2020-06-02 10:38:32 +02:00
Michael Mattiello (mcm) 9fd8315c53 [FIX] qweb: support of svg in t-raw
Before this commit, t-raw a string containing `<svg>` did not display
the svg element. This was because of missing namespace in vnode's data.

Now, `vdom.addNS` is called to add the svg namespace when necessary.
2020-05-25 14:02:34 +02:00
Géry Debongnie 85318b3ae6 [FIX] qweb: properly handle subtemplates in shared templates
With QWeb, we can register globally templates (using the xml tag or
the registerTemplate function). However, these templates, once
compiled, can generate sub template compiled functions. Before this
commit, these sub functions were local to a specific instance.

This means that creating a new QWeb instance and rendering a global
parent template would crash, since it was unable to find the actual sub
function.

This commit fixes the issue: the sub functions are now shared
statically, but with a unique ID, so we do not have issues with sub
functions having a same name in different QWeb instance.

closes #701
2020-05-25 11:43:24 +02:00
Géry Debongnie 4b961cbffe [REL] v1.0.8
# v1.0.8

- qweb fix: do not override t-att-class with class attribute in some cases
- component fix: refuse to mount a destroyed component
- hooks type improvement: useRef is now generic, and can capture the type of a component
- qweb fix: t-esc inside t-call using outside t-set
2020-05-18 09:07:31 +02:00
Aaron Bohy 2af8cccd65 [FIX] qweb: t-esc inside t-call using outside t-set
Before this commit, the generated code was incorrect, and crashed,
when there was a t-esc="abc" in a subtemplate, with t-set="abc"
done outside the subtemplate, with syntax <t t-set="abc">value</t>.
2020-05-15 13:47:57 +02:00
Géry Debongnie b8d3618afa [IMP] hooks: improve useRef typings
With this commit, we can get the type for the target component for a
ref.

By default, if the generic type is not given, the ref will simply use
the base Component type.
2020-04-30 16:23:36 +02:00
Géry Debongnie 559fadb62a [FIX] component: properly handle mounting a destroyed component
part of #685
2020-04-22 09:29:39 +02:00
Géry Debongnie c36333dbbc [CLEANUP] update prettier to v2.0.4 2020-04-21 16:23:31 +02:00
Géry Debongnie b5c3422b4d [FIX] qweb: do not override t-att-class with class
There was some code in qweb to make sure that we support setting class
and t-att-class on the same html element:

<div class="some class" t-att-class="{b: true}">...</div>

But the code did not work in the other direction:

<div t-att-class="{b: true}" class="some class">...</div>

With this commit, we just add the missing if statement

closes #664
2020-04-21 16:23:31 +02:00
Géry Debongnie 23ce19e57a [REL] v1.0.7 2020-04-17 15:52:52 +02:00
Géry Debongnie aaf40e9bec [IMP] tooling: add iife build to npm package
This is useful to link to the iife version in some other projects.
2020-04-17 15:47:20 +02:00
74 changed files with 2945 additions and 1292 deletions
+26
View File
@@ -0,0 +1,26 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test
+2 -2
View File
@@ -121,8 +121,8 @@ npm install @odoo/owl
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-1.0.6.js](https://github.com/odoo/owl/releases/download/v1.0.6/owl.js) - [owl-1.0.11.js](https://github.com/odoo/owl/releases/download/v1.0.11/owl.js)
- [owl-1.0.6.min.js](https://github.com/odoo/owl/releases/download/v1.0.6/owl.min.js) - [owl-1.0.11.min.js](https://github.com/odoo/owl/releases/download/v1.0.11/owl.min.js)
## License ## License
+1 -1
View File
@@ -111,7 +111,7 @@ class Parent extends Component {
static components = { OrderLine }; static components = { OrderLine };
orders = useState([ orders = useState([
{ id: 1, name: "Coffee", quantity: 0 }, { id: 1, name: "Coffee", quantity: 0 },
{ id: 2, name: "Tea", quantity: 0 } { id: 2, name: "Tea", quantity: 0 },
]); ]);
addToOrder(event) { addToOrder(event) {
+11 -11
View File
@@ -281,7 +281,7 @@ import { Component } from "@odoo/owl";
import "regenerator-runtime/runtime"; import "regenerator-runtime/runtime";
export async function nextTick() { export async function nextTick() {
return new Promise(function(resolve) { return new Promise(function (resolve) {
setTimeout(() => Component.scheduler.requestAnimationFrame(() => resolve())); setTimeout(() => Component.scheduler.requestAnimationFrame(() => resolve()));
}); });
} }
@@ -361,26 +361,26 @@ const HtmlWebpackPlugin = require("html-webpack-plugin");
const host = process.env.HOST || "localhost"; const host = process.env.HOST || "localhost";
module.exports = function(env, argv) { module.exports = function (env, argv) {
const mode = argv.mode || "development"; const mode = argv.mode || "development";
return { return {
mode: mode, mode: mode,
entry: "./src/main.js", entry: "./src/main.js",
output: { output: {
filename: "main.js", filename: "main.js",
path: path.resolve(__dirname, "dist") path: path.resolve(__dirname, "dist"),
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.jsx?$/, test: /\.jsx?$/,
loader: "babel-loader", loader: "babel-loader",
exclude: /node_modules/ exclude: /node_modules/,
} },
] ],
}, },
resolve: { resolve: {
extensions: [".js", ".jsx"] extensions: [".js", ".jsx"],
}, },
devServer: { devServer: {
contentBase: path.resolve(__dirname, "public/index.html"), contentBase: path.resolve(__dirname, "public/index.html"),
@@ -388,14 +388,14 @@ module.exports = function(env, argv) {
hot: true, hot: true,
host, host,
port: 3000, port: 3000,
publicPath: "/" publicPath: "/",
}, },
plugins: [ plugins: [
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
inject: true, inject: true,
template: path.resolve(__dirname, "public/index.html") template: path.resolve(__dirname, "public/index.html"),
}) }),
] ],
}; };
}; };
``` ```
+19 -19
View File
@@ -62,7 +62,7 @@ our application. `app.js` is where we will write all our code. For now, let's
just put the following code: just put the following code:
```js ```js
(function() { (function () {
console.log("hello owl", owl.__info__.version); console.log("hello owl", owl.__info__.version);
})(); })();
``` ```
@@ -170,13 +170,13 @@ class App extends Component {
{ {
id: 1, id: 1,
title: "buy milk", title: "buy milk",
isCompleted: true isCompleted: true,
}, },
{ {
id: 2, id: 2,
title: "clean house", title: "clean house",
isCompleted: false isCompleted: false,
} },
]; ];
} }
``` ```
@@ -562,23 +562,23 @@ const actions = {
const task = { const task = {
id: state.nextId++, id: state.nextId++,
title: title, title: title,
isCompleted: false isCompleted: false,
}; };
state.tasks.push(task); state.tasks.push(task);
} }
}, },
toggleTask({ state }, id) { toggleTask({ state }, id) {
const task = state.tasks.find(t => t.id === id); const task = state.tasks.find((t) => t.id === id);
task.isCompleted = !task.isCompleted; task.isCompleted = !task.isCompleted;
}, },
deleteTask({ state }, id) { deleteTask({ state }, id) {
const index = state.tasks.findIndex(t => t.id === id); const index = state.tasks.findIndex((t) => t.id === id);
state.tasks.splice(index, 1); state.tasks.splice(index, 1);
} },
}; };
const initialState = { const initialState = {
nextId: 1, nextId: 1,
tasks: [] tasks: [],
}; };
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -616,7 +616,7 @@ class App extends Component {
static components = { Task }; static components = { Task };
inputRef = useRef("add-input"); inputRef = useRef("add-input");
tasks = useStore(state => state.tasks); tasks = useStore((state) => state.tasks);
dispatch = useDispatch(); dispatch = useDispatch();
mounted() { mounted() {
@@ -811,7 +811,7 @@ For reference, here is the final code:
``` ```
```js ```js
(function() { (function () {
const { Component, Store } = owl; const { Component, Store } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
const { whenReady } = owl.utils; const { whenReady } = owl.utils;
@@ -827,24 +827,24 @@ For reference, here is the final code:
const task = { const task = {
id: state.nextId++, id: state.nextId++,
title: title, title: title,
isCompleted: false isCompleted: false,
}; };
state.tasks.push(task); state.tasks.push(task);
} }
}, },
toggleTask({ state }, id) { toggleTask({ state }, id) {
const task = state.tasks.find(t => t.id === id); const task = state.tasks.find((t) => t.id === id);
task.isCompleted = !task.isCompleted; task.isCompleted = !task.isCompleted;
}, },
deleteTask({ state }, id) { deleteTask({ state }, id) {
const index = state.tasks.findIndex(t => t.id === id); const index = state.tasks.findIndex((t) => t.id === id);
state.tasks.splice(index, 1); state.tasks.splice(index, 1);
} },
}; };
const initialState = { const initialState = {
nextId: 1, nextId: 1,
tasks: [] tasks: [],
}; };
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -897,7 +897,7 @@ For reference, here is the final code:
static components = { Task }; static components = { Task };
inputRef = useRef("add-input"); inputRef = useRef("add-input");
tasks = useStore(state => state.tasks); tasks = useStore((state) => state.tasks);
filter = useState({ value: "all" }); filter = useState({ value: "all" });
dispatch = useDispatch(); dispatch = useDispatch();
@@ -916,9 +916,9 @@ For reference, here is the final code:
get displayedTasks() { get displayedTasks() {
switch (this.filter.value) { switch (this.filter.value) {
case "active": case "active":
return this.tasks.filter(t => !t.isCompleted); return this.tasks.filter((t) => !t.isCompleted);
case "completed": case "completed":
return this.tasks.filter(t => t.isCompleted); return this.tasks.filter((t) => t.isCompleted);
case "all": case "all":
return this.tasks; return this.tasks;
} }
+3 -3
View File
@@ -257,11 +257,11 @@ to the store like in redux, with the `useStore` hook (see the [store documentati
const actions = { const actions = {
increment({ state }, val) { increment({ state }, val) {
state.counter.value += val; state.counter.value += val;
} },
}; };
const state = { const state = {
counter: { value: 0 } counter: { value: 0 },
}; };
const store = new owl.Store({ state, actions }); const store = new owl.Store({ state, actions });
@@ -270,7 +270,7 @@ class Counter extends Component {
<button t-name="Counter" t-on-click="dispatch('increment')"> <button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="counter.value"/>] Click Me! [<t t-esc="counter.value"/>]
</button>`; </button>`;
counter = useStore(state => state.counter); counter = useStore((state) => state.counter);
dispatch = useDispatch(); dispatch = useDispatch();
} }
+4 -4
View File
@@ -204,7 +204,7 @@ to be called in the constructor.
class Counter extends owl.Component { class Counter extends owl.Component {
static props = { static props = {
initialValue: Number, initialValue: Number,
optional: true optional: true,
}; };
} }
``` ```
@@ -217,7 +217,7 @@ to be called in the constructor.
```js ```js
class Counter extends owl.Component { class Counter extends owl.Component {
static defaultProps = { static defaultProps = {
initialValue: 0 initialValue: 0,
}; };
} }
``` ```
@@ -804,8 +804,8 @@ class RootNode extends Component {
children: [ children: [
{ label: "b" }, { label: "b" },
{ label: "c", children: [{ label: "d" }, { label: "e" }] }, { label: "c", children: [{ label: "d" }, { label: "e" }] },
{ label: "f", children: [{ label: "g" }] } { label: "f", children: [{ label: "g" }] },
] ],
}; };
} }
``` ```
+2 -2
View File
@@ -113,10 +113,10 @@ async function myEnv() {
qweb: qweb, qweb: qweb,
services: { services: {
localStorage: localStorage, localStorage: localStorage,
rpc: rpc rpc: rpc,
}, },
debug: false, debug: false,
inMobileMode: true inMobileMode: true,
}; };
} }
+1 -1
View File
@@ -74,7 +74,7 @@ broadcasted to the application by an event on the `qweb` instance. It may be
useful, for example, to log the error somewhere. useful, for example, to log the error somewhere.
```js ```js
env.qweb.on("error", null, function(error) { env.qweb.on("error", null, function (error) {
// do something // do something
// react to the error // react to the error
}); });
+1 -1
View File
@@ -7,7 +7,7 @@ triggering events, and callbacks.
```js ```js
const bus = new owl.core.EventBus(); const bus = new owl.core.EventBus();
bus.on("some-event", null, function(...args) { bus.on("some-event", null, function (...args) {
console.log(...args); console.log(...args);
}); });
+1 -1
View File
@@ -259,7 +259,7 @@ function useLoader() {
} }
onWillStart(() => updateRecord(component.props.id)); onWillStart(() => updateRecord(component.props.id));
onWillUpdateProps(nextProps => updateRecord(nextProps.id)); onWillUpdateProps((nextProps) => updateRecord(nextProps.id));
return record; return record;
} }
+1 -1
View File
@@ -30,7 +30,7 @@ class ComponentB extends owl.Component {
count: {type: Number}, count: {type: Number},
messages: { messages: {
type: Array, type: Array,
element: {type: Object, shape: {id: Boolean, text: 'string' } element: {type: Object, shape: {id: Boolean, text: String }
}, },
date: Date, date: Date,
combinedVal: [Number, Boolean] combinedVal: [Number, Boolean]
+2 -2
View File
@@ -114,9 +114,9 @@ For example:
const translations = { const translations = {
hello: "bonjour", hello: "bonjour",
yes: "oui", yes: "oui",
no: "non" no: "non",
}; };
const translateFn = str => translations[str] || str; const translateFn = (str) => translations[str] || str;
const qweb = new QWeb({ translateFn }); const qweb = new QWeb({ translateFn });
``` ```
+14 -14
View File
@@ -40,14 +40,14 @@ const actions = {
state.todos.push({ state.todos.push({
id: state.nextId++, id: state.nextId++,
message, message,
isCompleted: false isCompleted: false,
}); });
} },
}; };
const state = { const state = {
todos: [], todos: [],
nextId: 1 nextId: 1,
}; };
const store = new owl.Store({ state, actions }); const store = new owl.Store({ state, actions });
@@ -90,7 +90,7 @@ const config = {
state, state,
actions, actions,
getters, getters,
env env,
}; };
const store = new Store(config); const store = new Store(config);
``` ```
@@ -110,7 +110,7 @@ const actions = {
} catch (e) { } catch (e) {
state.loginState = "error"; state.loginState = "error";
} }
} },
}; };
``` ```
@@ -144,7 +144,7 @@ const actions = {
state.recordId = recordId; state.recordId = recordId;
const data = await doSomeRPC("/read/", recordId); const data = await doSomeRPC("/read/", recordId);
state.recordData = data; state.recordData = data;
} },
}; };
``` ```
@@ -159,7 +159,7 @@ const actions = {
const data = await doSomeRPC("/read/", recordId); const data = await doSomeRPC("/read/", recordId);
state.recordId = recordId; state.recordId = recordId;
state.recordData = data; state.recordData = data;
} },
}; };
``` ```
@@ -187,14 +187,14 @@ transform the data contained in the store.
```js ```js
const getters = { const getters = {
getPost({ state }, id) { getPost({ state }, id) {
const post = state.posts.find(p => p.id === id); const post = state.posts.find((p) => p.id === id);
const author = state.authors.find(a => a.id === post.id); const author = state.authors.find((a) => a.id === post.id);
return { return {
id, id,
author, author,
content: post.content content: post.content,
}; };
} },
}; };
// somewhere else // somewhere else
@@ -224,11 +224,11 @@ Assume we have this store:
const actions = { const actions = {
increment({ state }, val) { increment({ state }, val) {
state.counter.value += val; state.counter.value += val;
} },
}; };
const state = { const state = {
counter: { value: 0 } counter: { value: 0 },
}; };
const store = new owl.Store({ state, actions }); const store = new owl.Store({ state, actions });
``` ```
@@ -245,7 +245,7 @@ A counter component can then select this value and dispatch an action like this:
```js ```js
class Counter extends Component { class Counter extends Component {
counter = useStore(state => state.counter); counter = useStore((state) => state.counter);
dispatch = useDispatch(); dispatch = useDispatch();
} }
+2 -2
View File
@@ -19,7 +19,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([loadFile("templates.xml"), owl.utils.whenReady()]).then(function([templates]) { Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function ([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);
@@ -29,7 +29,7 @@ Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function([t
or alternatively: or alternatively:
```js ```js
owl.utils.whenReady(function() { owl.utils.whenReady(function () {
const qweb = new owl.QWeb(); const qweb = new owl.QWeb();
const app = new App({ qweb }); const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
+8 -5
View File
@@ -1,12 +1,13 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "1.0.6", "version": "1.0.11",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl.js", "main": "dist/owl.js",
"types": "dist/types/index.d.ts", "types": "dist/types/index.d.ts",
"files": [ "files": [
"dist/types/", "dist/types/",
"dist/owl.js" "dist/owl.js",
"dist/owl-iife.js"
], ],
"engines": { "engines": {
"node": ">=10.15.3" "node": ">=10.15.3"
@@ -24,7 +25,7 @@
"pretools:watch": "npm run build", "pretools:watch": "npm run build",
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"", "tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write", "prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
"publish": "npm run buildcommonjs && npm publish", "publish": "npm run build && mv dist/owl.js dist/owl-iife.js && npm run buildcommonjs && npm publish",
"release": "node tools/release.js" "release": "node tools/release.js"
}, },
"repository": { "repository": {
@@ -47,8 +48,9 @@
"jest": "^23.6.0", "jest": "^23.6.0",
"jest-environment-jsdom": "^24.7.1", "jest-environment-jsdom": "^24.7.1",
"live-server": "^1.2.1", "live-server": "^1.2.1",
"monaco-editor": "^0.21.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^1.19.1", "prettier": "^2.0.4",
"rollup": "^1.6.0", "rollup": "^1.6.0",
"rollup-plugin-typescript2": "^0.20.1", "rollup-plugin-typescript2": "^0.20.1",
"sass": "^1.16.1", "sass": "^1.16.1",
@@ -77,6 +79,7 @@
] ]
}, },
"prettier": { "prettier": {
"printWidth": 100 "printWidth": 100,
"endOfLine": "auto"
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉 # 🦉 OWL Roadmap 🦉
- Current version: 1.0.6 - Current version: 1.0.11
- Status: stable - Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may This roadmap is only an attempt at predicting Owl's future. Everything may
+1 -1
View File
@@ -19,5 +19,5 @@ export const browser: Browser = {
random: Math.random, random: Math.random,
Date: window.Date, Date: window.Date,
fetch: (window.fetch || (() => {})).bind(window), fetch: (window.fetch || (() => {})).bind(window),
localStorage: window.localStorage localStorage: window.localStorage,
}; };
+32 -10
View File
@@ -203,7 +203,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
renderFn: qweb.render.bind(qweb, template), renderFn: qweb.render.bind(qweb, template),
classObj: null, classObj: null,
refs: null, refs: null,
scope: null scope: null,
}; };
if (constr.style) { if (constr.style) {
this.__applyStyles(constr); this.__applyStyles(constr);
@@ -316,6 +316,9 @@ export class Component<Props extends {} = any, T extends Env = Env> {
return Promise.resolve(); return Promise.resolve();
} }
} }
if (__owl__.isDestroyed) {
throw new Error("Cannot mount a destroyed component");
}
if (__owl__.currentFiber) { if (__owl__.currentFiber) {
const currentFiber = __owl__.currentFiber; const currentFiber = __owl__.currentFiber;
if (currentFiber.target === target && currentFiber.position === position) { if (currentFiber.target === target && currentFiber.position === position) {
@@ -493,9 +496,9 @@ export class Component<Props extends {} = any, T extends Env = Env> {
} }
this.willUnmount(); this.willUnmount();
__owl__.isMounted = false; __owl__.isMounted = false;
if (this.__owl__.currentFiber) { if (__owl__.currentFiber) {
this.__owl__.currentFiber.isCompleted = true; __owl__.currentFiber.isCompleted = true;
this.__owl__.currentFiber.root.counter = 0; __owl__.currentFiber.root.counter = 0;
} }
const children = __owl__.children; const children = __owl__.children;
for (let id in children) { for (let id in children) {
@@ -514,7 +517,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
const ev = new OwlEvent(component, eventType, { const ev = new OwlEvent(component, eventType, {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
detail: payload detail: payload,
}); });
const triggerHook = this.env[portalSymbol as any]; const triggerHook = this.env[portalSymbol as any];
if (triggerHook) { if (triggerHook) {
@@ -549,7 +552,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
} }
await Promise.all([ await Promise.all([
this.willUpdateProps(nextProps), this.willUpdateProps(nextProps),
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps) __owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps),
]); ]);
if (fiber.isCompleted) { if (fiber.isCompleted) {
return; return;
@@ -646,16 +649,35 @@ export class Component<Props extends {} = any, T extends Env = Env> {
try { try {
let vnode = __owl__.renderFn!(this, { let vnode = __owl__.renderFn!(this, {
handlers: __owl__.boundHandlers, handlers: __owl__.boundHandlers,
fiber: fiber fiber: fiber,
}); });
// we iterate over the children to detect those that no longer belong to the // we iterate over the children to detect those that no longer belong to the
// current rendering: those ones, if not mounted yet, can (and have to) be // current rendering: those ones, if not mounted yet, can (and have to) be
// destroyed right now, because they are not in the DOM, and thus we won't // destroyed right now, because they are not in the DOM, and thus we won't
// be notified later on (when patching), that they are removed from the DOM // be notified later on (when patching), that they are removed from the DOM
for (let childKey in __owl__.children) { for (let childKey in __owl__.children) {
let child = __owl__.children[childKey]; const child = __owl__.children[childKey];
if (!child.__owl__.isMounted && child.__owl__.parentLastFiberId < fiber.id) { const childOwl = child.__owl__;
child.destroy(); if (!childOwl.isMounted && childOwl.parentLastFiberId < fiber.id) {
// we only do here a "soft" destroy, meaning that we leave the child
// dom node alone, without removing it. Most of the time, it does not
// matter, because the child component is already unmounted. However,
// if some of its parent have been unmounted, the child could actually
// still be attached to its parent, and this may be important if we
// want to remount the parent, because the vdom need to match the
// actual DOM
child.__destroy(childOwl.parent);
if (childOwl.pvnode) {
// we remove the key here to make sure that the patching algorithm
// is able to make the difference between this pvnode and an eventual
// other instance of the same component
delete childOwl.pvnode.key;
// Since the component has been unmounted, we do not want to actually
// call a remove hook. This is pretty important, since the t-component
// directive actually disabled it, so the vdom algorithm will just
// not remove the child elm if we don't remove the hook.
delete childOwl.pvnode.data!.hook!.remove;
}
} }
} }
if (!vnode) { if (!vnode) {
+35 -19
View File
@@ -7,7 +7,7 @@ import { makeHandlerCode, MODS_CODE } from "../qweb/extensions";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, { const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
self: "if (e.target !== vn.elm) {return}" self: "if (e.target !== vn.elm) {return}",
}); });
QWeb.utils.defineProxy = function defineProxy(target, source) { QWeb.utils.defineProxy = function defineProxy(target, source) {
@@ -18,7 +18,7 @@ QWeb.utils.defineProxy = function defineProxy(target, source) {
}, },
set(val) { set(val) {
source[k] = val; source[k] = val;
} },
}); });
} }
}; };
@@ -238,7 +238,7 @@ QWeb.addDirective({
// computing the props string representing the props object // computing the props string representing the props object
let propStr = Object.keys(props) let propStr = Object.keys(props)
.map(k => k + ":" + props[k]) .map((k) => k + ":" + props[k])
.join(","); .join(",");
let componentID = ctx.generateID(); let componentID = ctx.generateID();
@@ -280,7 +280,7 @@ QWeb.addDirective({
let classDef = classAttr let classDef = classAttr
.trim() .trim()
.split(/\s+/) .split(/\s+/)
.map(a => `'${a}':true`) .map((a) => `'${a}':true`)
.join(","); .join(",");
classObj = `_${ctx.generateID()}`; classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`); ctx.addLine(`let ${classObj} = {${classDef}};`);
@@ -298,7 +298,7 @@ QWeb.addDirective({
} }
} }
let eventsCode = events let eventsCode = events
.map(function([name, value]) { .map(function ([name, value]) {
const capture = name.match(/\.capture/); const capture = name.match(/\.capture/);
name = capture ? name.replace(/\.capture/, "") : name; name = capture ? name.replace(/\.capture/, "") : name;
const { event, handler } = makeHandlerCode( const { event, handler } = makeHandlerCode(
@@ -400,7 +400,6 @@ QWeb.addDirective({
if (hasSlots) { if (hasSlots) {
const clone = <Element>node.cloneNode(true); const clone = <Element>node.cloneNode(true);
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
// The next code is a fallback for compatibility reason. It accepts t-set // The next code is a fallback for compatibility reason. It accepts t-set
// elements that are direct children with a non empty body as nodes defining // elements that are direct children with a non empty body as nodes defining
@@ -410,26 +409,43 @@ QWeb.addDirective({
// code using slots. This will be removed in v2.0 someday. Meanwhile, // code using slots. This will be removed in v2.0 someday. Meanwhile,
// please use t-set-slot everywhere you need to set the content of a // please use t-set-slot everywhere you need to set the content of a
// slot. // slot.
for (let el of clone.children) { for (let node of clone.children) {
if (el.getAttribute("t-set") && el.hasChildNodes()) { if (node.hasAttribute("t-set") && node.hasChildNodes()) {
slotNodes.push(el); node.setAttribute("t-set-slot", node.getAttribute("t-set")!);
node.removeAttribute("t-set");
} }
} }
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
const slotNames = new Set<string>();
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++) {
const slotNode = slotNodes[i]; const slotNode = slotNodes[i];
slotNode.parentElement!.removeChild(slotNode); // check if this is defined in a sub component (in which case it should
let key = slotNode.getAttribute("t-set-slot")!; // be ignored)
slotNode.removeAttribute("t-set-slot"); let el = slotNode.parentElement;
let isInSubComponent = false;
// here again, this code should be removed when we stop supporting while (el !== clone) {
// using t-set to define the content of named slots. if (
if (!key) { el!.hasAttribute("t-component") ||
key = slotNode.getAttribute("t-set")!; el!.tagName[0] === el!.tagName[0].toUpperCase()
slotNode.removeAttribute("t-set"); ) {
isInSubComponent = true;
break;
}
} }
if (isInSubComponent) {
continue;
}
let key = slotNode.getAttribute("t-set-slot")!;
if (slotNames.has(key)) {
continue;
}
slotNames.add(key);
slotNode.removeAttribute("t-set-slot");
slotNode.parentElement!.removeChild(slotNode);
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx); const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
QWeb.slots[`${slotId}_${key}`] = slotFn; QWeb.slots[`${slotId}_${key}`] = slotFn;
} }
@@ -469,5 +485,5 @@ QWeb.addDirective({
ctx.addLine(`w${componentID}.__owl__.parentLastFiberId = extra.fiber.id;`); ctx.addLine(`w${componentID}.__owl__.parentLastFiberId = extra.fiber.id;`);
return true; return true;
} },
}); });
+11 -3
View File
@@ -193,7 +193,7 @@ export class Fiber {
// build patchQueue // build patchQueue
const patchQueue: Fiber[] = []; const patchQueue: Fiber[] = [];
const doWork: (Fiber) => Fiber | null = function(f) { const doWork: (Fiber) => Fiber | null = function (f) {
patchQueue.push(f); patchQueue.push(f);
return f.child; return f.child;
}; };
@@ -238,12 +238,20 @@ export class Fiber {
} else { } else {
if (fiber.shouldPatch) { if (fiber.shouldPatch) {
component.__patch(component.__owl__.vnode!, fiber.vnode!); component.__patch(component.__owl__.vnode!, fiber.vnode!);
// When updating a Component's props (in directive),
// the component has a pvnode AND should be patched.
// However, its pvnode.elm may have changed if it is a High Order Component
if (component.__owl__.pvnode) {
component.__owl__.pvnode.elm = component.__owl__.vnode!.elm;
}
} else { } else {
component.__patch(document.createElement(fiber.vnode!.sel!), fiber.vnode!); component.__patch(document.createElement(fiber.vnode!.sel!), fiber.vnode!);
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm; component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
} }
} }
component.__owl__.currentFiber = null; if (fiber === component.__owl__.currentFiber) {
component.__owl__.currentFiber = null;
}
} }
// insert into the DOM (mount case) // insert into the DOM (mount case)
@@ -280,7 +288,7 @@ export class Fiber {
* Cancel a fiber and all its children. * Cancel a fiber and all its children.
*/ */
cancel() { cancel() {
this._walk(f => { this._walk((f) => {
if (!f.isRendered) { if (!f.isRendered) {
f.root.counter--; f.root.counter--;
} }
+2 -2
View File
@@ -11,7 +11,7 @@ import { QWeb } from "../qweb/index";
* This is why it is only done in 'dev' mode. * This is why it is only done in 'dev' mode.
*/ */
QWeb.utils.validateProps = function(Widget, props: Object) { QWeb.utils.validateProps = function (Widget, props: Object) {
const propsDef = (<any>Widget).props; const propsDef = (<any>Widget).props;
if (propsDef instanceof Array) { if (propsDef instanceof Array) {
// list of strings (prop names) // list of strings (prop names)
@@ -37,7 +37,7 @@ QWeb.utils.validateProps = function(Widget, props: Object) {
if (propsDef[propName] && !propsDef[propName].optional) { if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(`Missing props '${propName}' (component '${Widget.name}')`); throw new Error(`Missing props '${propName}' (component '${Widget.name}')`);
} else { } else {
break; continue;
} }
} }
let isValid; let isValid;
+3 -3
View File
@@ -50,7 +50,7 @@ export class Scheduler {
return reject(fiber.error); return reject(fiber.error);
} }
resolve(); resolve();
} },
}); });
if (!this.isRunning) { if (!this.isRunning) {
this.start(); this.start();
@@ -60,7 +60,7 @@ export class Scheduler {
rejectFiber(fiber: Fiber, reason: string) { rejectFiber(fiber: Fiber, reason: string) {
fiber = fiber.root; fiber = fiber.root;
const index = this.tasks.findIndex(t => t.fiber === fiber); const index = this.tasks.findIndex((t) => t.fiber === fiber);
if (index >= 0) { if (index >= 0) {
const [task] = this.tasks.splice(index, 1); const [task] = this.tasks.splice(index, 1);
fiber.cancel(); fiber.cancel();
@@ -76,7 +76,7 @@ export class Scheduler {
flush() { flush() {
let tasks = this.tasks; let tasks = this.tasks;
this.tasks = []; this.tasks = [];
tasks = tasks.filter(task => { tasks = tasks.filter((task) => {
if (task.fiber.isCompleted) { if (task.fiber.isCompleted) {
task.callback(); task.callback();
return false; return false;
+1 -1
View File
@@ -8,7 +8,7 @@
export const STYLESHEETS: { [id: string]: HTMLStyleElement } = {}; export const STYLESHEETS: { [id: string]: HTMLStyleElement } = {};
export function processSheet(str: string): string { export function processSheet(str: string): string {
const tokens = str.split(/(\{|\}|;)/).map(s => s.trim()); const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
const selectorStack: string[][] = []; const selectorStack: string[][] = [];
const parts: string[] = []; const parts: string[] = [];
let rules: string[] = []; let rules: string[] = [];
+1 -1
View File
@@ -26,5 +26,5 @@ Object.defineProperty(config, "mode", {
} else { } else {
console.log(`Owl is now running in 'prod' mode.`); console.log(`Owl is now running in 'prod' mode.`);
} }
} },
}); });
+6 -6
View File
@@ -79,9 +79,9 @@ export class Context extends EventBus {
async __notifyComponents() { async __notifyComponents() {
const rev = ++this.rev; const rev = ++this.rev;
const subscriptions = this.subscriptions.update; const subscriptions = this.subscriptions.update;
const groups = partitionBy(subscriptions, s => (s.owner ? s.owner.__owl__.depth : -1)); const groups = partitionBy(subscriptions, (s) => (s.owner ? s.owner.__owl__.depth : -1));
for (let group of groups) { for (let group of groups) {
const proms = group.map(sub => sub.callback.call(sub.owner, rev)); const proms = group.map((sub) => sub.callback.call(sub.owner, rev));
// at this point, each component in the current group has registered a // at this point, each component in the current group has registered a
// top level fiber in the scheduler. It could happen that rendering these // top level fiber in the scheduler. It could happen that rendering these
// components is done (if they have no children). This is why we manually // components is done (if they have no children). This is why we manually
@@ -116,7 +116,7 @@ export function useContextWithCB(ctx: Context, component: Component, method): an
__owl__.observer.notifyCB = component.render.bind(component); __owl__.observer.notifyCB = component.render.bind(component);
} }
const currentCB = __owl__.observer.notifyCB; const currentCB = __owl__.observer.notifyCB;
__owl__.observer.notifyCB = function() { __owl__.observer.notifyCB = function () {
if (ctx.rev > mapping[id]) { if (ctx.rev > mapping[id]) {
// in this case, the context has been updated since we were rendering // in this case, the context has been updated since we were rendering
// last, and we do not need to render here with the observer. A // last, and we do not need to render here with the observer. A
@@ -128,18 +128,18 @@ export function useContextWithCB(ctx: Context, component: Component, method): an
mapping[id] = 0; mapping[id] = 0;
const renderFn = __owl__.renderFn; const renderFn = __owl__.renderFn;
__owl__.renderFn = function(comp, params) { __owl__.renderFn = function (comp, params) {
mapping[id] = ctx.rev; mapping[id] = ctx.rev;
return renderFn(comp, params); return renderFn(comp, params);
}; };
ctx.on("update", component, async contextRev => { ctx.on("update", component, async (contextRev) => {
if (mapping[id] < contextRev) { if (mapping[id] < contextRev) {
mapping[id] = contextRev; mapping[id] = contextRev;
await method(); await method();
} }
}); });
const __destroy = component.__destroy; const __destroy = component.__destroy;
component.__destroy = parent => { component.__destroy = (parent) => {
ctx.off("update", component); ctx.off("update", component);
delete mapping[id]; delete mapping[id];
__destroy.call(component, parent); __destroy.call(component, parent);
+2 -2
View File
@@ -44,7 +44,7 @@ export class EventBus {
} }
this.subscriptions[eventType].push({ this.subscriptions[eventType].push({
owner, owner,
callback callback,
}); });
} }
@@ -54,7 +54,7 @@ export class EventBus {
off(eventType: string, owner: any) { off(eventType: string, owner: any) {
const subs = this.subscriptions[eventType]; const subs = this.subscriptions[eventType];
if (subs) { if (subs) {
this.subscriptions[eventType] = subs.filter(s => s.owner !== owner); this.subscriptions[eventType] = subs.filter((s) => s.owner !== owner);
} }
} }
+8 -3
View File
@@ -25,7 +25,12 @@ export class Observer {
notifyCB() {} notifyCB() {}
observe<T>(value: T, parent?: any): T { observe<T>(value: T, parent?: any): T {
if (value === null || typeof value !== "object" || value instanceof Date) { if (
value === null ||
typeof value !== "object" ||
value instanceof Date ||
value instanceof Promise
) {
// fun fact: typeof null === 'object' // fun fact: typeof null === 'object'
return value; return value;
} }
@@ -67,14 +72,14 @@ export class Observer {
self.notifyCB(); self.notifyCB();
} }
return true; return true;
} },
}); });
const metadata = { const metadata = {
value, value,
proxy, proxy,
rev: this.rev, rev: this.rev,
parent parent,
}; };
this.weakMap.set(value, metadata); this.weakMap.set(value, metadata);
+12 -12
View File
@@ -36,11 +36,11 @@ export function useState<T>(state: T): T {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
function makeLifecycleHook(method: string, reverse: boolean = false) { function makeLifecycleHook(method: string, reverse: boolean = false) {
if (reverse) { if (reverse) {
return function(cb) { return function (cb) {
const component: Component = Component.current!; const component: Component = Component.current!;
if (component.__owl__[method]) { if (component.__owl__[method]) {
const current = component.__owl__[method]; const current = component.__owl__[method];
component.__owl__[method] = function() { component.__owl__[method] = function () {
current.call(component); current.call(component);
cb.call(component); cb.call(component);
}; };
@@ -49,11 +49,11 @@ function makeLifecycleHook(method: string, reverse: boolean = false) {
} }
}; };
} else { } else {
return function(cb) { return function (cb) {
const component: Component = Component.current!; const component: Component = Component.current!;
if (component.__owl__[method]) { if (component.__owl__[method]) {
const current = component.__owl__[method]; const current = component.__owl__[method];
component.__owl__[method] = function() { component.__owl__[method] = function () {
cb.call(component); cb.call(component);
current.call(component); current.call(component);
}; };
@@ -65,11 +65,11 @@ function makeLifecycleHook(method: string, reverse: boolean = false) {
} }
function makeAsyncHook(method: string) { function makeAsyncHook(method: string) {
return function(cb) { return function (cb) {
const component: Component = Component.current!; const component: Component = Component.current!;
if (component.__owl__[method]) { if (component.__owl__[method]) {
const current = component.__owl__[method]; const current = component.__owl__[method];
component.__owl__[method] = function(...args) { component.__owl__[method] = function (...args) {
return Promise.all([current.call(component, ...args), cb.call(component, ...args)]); return Promise.all([current.call(component, ...args), cb.call(component, ...args)]);
}; };
} else { } else {
@@ -94,12 +94,12 @@ export const onWillUpdateProps = makeAsyncHook("willUpdatePropsCB");
* The purpose of this hook is to allow components to get a reference to a sub * The purpose of this hook is to allow components to get a reference to a sub
* html node or component. * html node or component.
*/ */
interface Ref { interface Ref<C extends Component = Component> {
el: HTMLElement | null; el: HTMLElement | null;
comp: Component | null; comp: C | null;
} }
export function useRef(name: string): Ref { export function useRef<C extends Component = Component>(name: string): Ref<C> {
const __owl__ = Component.current!.__owl__; const __owl__ = Component.current!.__owl__;
return { return {
get el(): HTMLElement | null { get el(): HTMLElement | null {
@@ -111,10 +111,10 @@ export function useRef(name: string): Ref {
} }
return null; return null;
}, },
get comp(): Component | null { get comp(): C | null {
const val = __owl__.refs && __owl__.refs[name]; const val = __owl__.refs && __owl__.refs[name];
return val instanceof Component ? val : null; return val instanceof Component ? (val as C) : null;
} },
}; };
} }
+1 -1
View File
@@ -35,6 +35,6 @@ export const hooks = Object.assign({}, _hooks, {
useContext: _context.useContext, useContext: _context.useContext,
useDispatch: _store.useDispatch, useDispatch: _store.useDispatch,
useGetters: _store.useGetters, useGetters: _store.useGetters,
useStore: _store.useStore useStore: _store.useStore,
}); });
export const __info__ = {}; export const __info__ = {};
+4 -4
View File
@@ -26,8 +26,8 @@ export class Portal extends Component<Props> {
static template = xml`<portal><t t-slot="default"/></portal>`; static template = xml`<portal><t t-slot="default"/></portal>`;
static props = { static props = {
target: { target: {
type: String type: String,
} },
}; };
// boolean to indicate whether or not we must listen to 'dom-appended' event // boolean to indicate whether or not we must listen to 'dom-appended' event
@@ -55,12 +55,12 @@ export class Portal extends Component<Props> {
// put a callback in the env that is propagated to children s.t. portal can // put a callback in the env that is propagated to children s.t. portal can
// register an handler to those events just before children will trigger them // register an handler to those events just before children will trigger them
useSubEnv({ useSubEnv({
[portalSymbol]: ev => { [portalSymbol]: (ev) => {
if (!this._handledEvents.has(ev.type)) { if (!this._handledEvents.has(ev.type)) {
this.portal!.elm!.addEventListener(ev.type, this._handlerTunnel); this.portal!.elm!.addEventListener(ev.type, this._handlerTunnel);
this._handledEvents.add(ev.type); this._handledEvents.add(ev.type);
} }
} },
}); });
} }
/** /**
+20 -22
View File
@@ -49,6 +49,7 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
if (ctx.escaping) { if (ctx.escaping) {
let protectID; let protectID;
if (value.hasBody) { if (value.hasBody) {
ctx.rootContext.shouldDefineUtils = true;
protectID = ctx.startProtectScope(); protectID = ctx.startProtectScope();
ctx.addLine( ctx.addLine(
`${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};` `${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};`
@@ -96,7 +97,7 @@ QWeb.addDirective({
let value = ctx.getValue(node.getAttribute("t-esc")!); let value = ctx.getValue(node.getAttribute("t-esc")!);
compileValueNode(value, node, qweb, ctx.subContext("escaping", true)); compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
return true; return true;
} },
}); });
QWeb.addDirective({ QWeb.addDirective({
@@ -106,7 +107,7 @@ QWeb.addDirective({
let value = ctx.getValue(node.getAttribute("t-raw")!); let value = ctx.getValue(node.getAttribute("t-raw")!);
compileValueNode(value, node, qweb, ctx); compileValueNode(value, node, qweb, ctx);
return true; return true;
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -163,7 +164,7 @@ QWeb.addDirective({
} }
} }
return true; return true;
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -179,7 +180,7 @@ QWeb.addDirective({
}, },
finalize({ ctx }) { finalize({ ctx }) {
ctx.closeIf(); ctx.closeIf();
} },
}); });
QWeb.addDirective({ QWeb.addDirective({
@@ -195,7 +196,7 @@ QWeb.addDirective({
}, },
finalize({ ctx }) { finalize({ ctx }) {
ctx.closeIf(); ctx.closeIf();
} },
}); });
QWeb.addDirective({ QWeb.addDirective({
@@ -208,7 +209,7 @@ QWeb.addDirective({
}, },
finalize({ ctx }) { finalize({ ctx }) {
ctx.closeIf(); ctx.closeIf();
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -222,9 +223,6 @@ QWeb.addDirective({
// ------------------------------------------------ // ------------------------------------------------
ctx.rootContext.shouldDefineScope = true; ctx.rootContext.shouldDefineScope = true;
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
if (node.nodeName !== "t") {
throw new Error("Invalid tag for t-call directive (should be 't')");
}
const subTemplate = node.getAttribute("t-call")!; const subTemplate = node.getAttribute("t-call")!;
const nodeTemplate = qweb.templates[subTemplate]; const nodeTemplate = qweb.templates[subTemplate];
if (!nodeTemplate) { if (!nodeTemplate) {
@@ -233,21 +231,22 @@ QWeb.addDirective({
// Step 2: compile target template in sub templates // Step 2: compile target template in sub templates
// ------------------------------------------------ // ------------------------------------------------
if (!qweb.subTemplates[subTemplate]) { let subId = qweb.subTemplates[subTemplate];
qweb.subTemplates[subTemplate] = true; if (!subId) {
subId = QWeb.nextId++;
qweb.subTemplates[subTemplate] = subId;
const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx, true); const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx, true);
qweb.subTemplates[subTemplate] = subTemplateFn; QWeb.subTemplates[subId] = subTemplateFn;
} }
// Step 3: compile t-call body if necessary // Step 3: compile t-call body if necessary
// ------------------------------------------------ // ------------------------------------------------
let hasBody = node.hasChildNodes(); let hasBody = node.hasChildNodes();
let protectID; const protectID = ctx.startProtectScope();
if (hasBody) { if (hasBody) {
// we add a sub scope to protect the ambient scope // we add a sub scope to protect the ambient scope
ctx.addLine(`{`); ctx.addLine(`{`);
ctx.indent(); ctx.indent();
protectID = ctx.startProtectScope();
const nodeCopy = node.cloneNode(true) as Element; const nodeCopy = node.cloneNode(true) as Element;
for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) { for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) {
nodeCopy.removeAttribute(attr); nodeCopy.removeAttribute(attr);
@@ -268,31 +267,30 @@ QWeb.addDirective({
// Step 4: add the appropriate function call to current component // Step 4: add the appropriate function call to current component
// ------------------------------------------------ // ------------------------------------------------
const callingScope = hasBody ? "scope" : "Object.assign(Object.create(context), scope)";
const parentComponent = `utils.getComponent(context)`; const parentComponent = `utils.getComponent(context)`;
const key = ctx.generateTemplateKey(); const key = ctx.generateTemplateKey();
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result"; const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`; const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`;
if (ctx.parentNode) { if (ctx.parentNode) {
ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, ${extra});`); ctx.addLine(`this.constructor.subTemplates['${subId}'].call(this, scope, ${extra});`);
} else { } else {
// this is a t-call with no parentnode, we need to extract the result // this is a t-call with no parentnode, we need to extract the result
ctx.rootContext.shouldDefineResult = true; ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`result = []`); ctx.addLine(`result = []`);
ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, ${extra});`); ctx.addLine(`this.constructor.subTemplates['${subId}'].call(this, scope, ${extra});`);
ctx.addLine(`result = result[0]`); ctx.addLine(`result = result[0]`);
} }
// Step 5: restore previous scope // Step 5: restore previous scope
// ------------------------------------------------ // ------------------------------------------------
if (hasBody) { if (hasBody) {
ctx.stopProtectScope(protectID);
ctx.dedent(); ctx.dedent();
ctx.addLine(`}`); ctx.addLine(`}`);
} }
ctx.stopProtectScope(protectID);
return true; return true;
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -353,7 +351,7 @@ QWeb.addDirective({
ctx.addLine("}"); ctx.addLine("}");
ctx.stopProtectScope(varsID); ctx.stopProtectScope(varsID);
return true; return true;
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -364,7 +362,7 @@ QWeb.addDirective({
priority: 1, priority: 1,
atNodeEncounter({ ctx }) { atNodeEncounter({ ctx }) {
ctx.addLine("debugger;"); ctx.addLine("debugger;");
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -376,5 +374,5 @@ QWeb.addDirective({
atNodeEncounter({ ctx, value }) { atNodeEncounter({ ctx, value }) {
const expr = ctx.formatExpression(value); const expr = ctx.formatExpression(value);
ctx.addLine(`console.log(${expr})`); ctx.addLine(`console.log(${expr})`);
} },
}); });
+2 -2
View File
@@ -162,7 +162,7 @@ export class CompilationContext {
const tokens = compileExprToArray(expr, this.variables); const tokens = compileExprToArray(expr, this.variables);
const done = new Set(); const done = new Set();
return tokens return tokens
.map(tok => { .map((tok) => {
if (tok.varName) { if (tok.varName) {
if (!done.has(tok.varName)) { if (!done.has(tok.varName)) {
done.add(tok.varName); done.add(tok.varName);
@@ -189,7 +189,7 @@ export class CompilationContext {
return `(${this.formatExpression(s.slice(2, -2))})`; return `(${this.formatExpression(s.slice(2, -2))})`;
} }
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}"); let r = s.replace(/\{\{.*?\}\}/g, (s) => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`"; return "`" + r + "`";
} }
startProtectScope(codeBlock?: boolean): number { startProtectScope(codeBlock?: boolean): number {
+9 -9
View File
@@ -35,7 +35,7 @@ const WORD_REPLACEMENT = {
gt: ">", gt: ">",
gte: ">=", gte: ">=",
lt: "<", lt: "<",
lte: "<=" lte: "<=",
}; };
export interface QWebVar { export interface QWebVar {
@@ -77,7 +77,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
":": "COLON", ":": "COLON",
",": "COMMA", ",": "COMMA",
"(": "LEFT_PAREN", "(": "LEFT_PAREN",
")": "RIGHT_PAREN" ")": "RIGHT_PAREN",
}; };
// note that the space after typeof is relevant. It makes sure that the formatted // note that the space after typeof is relevant. It makes sure that the formatted
@@ -86,7 +86,7 @@ const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,
type Tokenizer = (expr: string) => Token | false; type Tokenizer = (expr: string) => Token | false;
let tokenizeString: Tokenizer = function(expr) { let tokenizeString: Tokenizer = function (expr) {
let s = expr[0]; let s = expr[0];
let start = s; let start = s;
if (s !== "'" && s !== '"') { if (s !== "'" && s !== '"') {
@@ -114,7 +114,7 @@ let tokenizeString: Tokenizer = function(expr) {
return { type: "VALUE", value: s }; return { type: "VALUE", value: s };
}; };
let tokenizeNumber: Tokenizer = function(expr) { let tokenizeNumber: Tokenizer = function (expr) {
let s = expr[0]; let s = expr[0];
if (s && s.match(/[0-9]/)) { if (s && s.match(/[0-9]/)) {
let i = 1; let i = 1;
@@ -128,7 +128,7 @@ let tokenizeNumber: Tokenizer = function(expr) {
} }
}; };
let tokenizeSymbol: Tokenizer = function(expr) { let tokenizeSymbol: Tokenizer = function (expr) {
let s = expr[0]; let s = expr[0];
if (s && s.match(/[a-zA-Z_\$]/)) { if (s && s.match(/[a-zA-Z_\$]/)) {
let i = 1; let i = 1;
@@ -145,7 +145,7 @@ let tokenizeSymbol: Tokenizer = function(expr) {
} }
}; };
const tokenizeStatic: Tokenizer = function(expr) { const tokenizeStatic: Tokenizer = function (expr) {
const char = expr[0]; const char = expr[0];
if (char && char in STATIC_TOKEN_MAP) { if (char && char in STATIC_TOKEN_MAP) {
return { type: STATIC_TOKEN_MAP[char], value: char }; return { type: STATIC_TOKEN_MAP[char], value: char };
@@ -153,7 +153,7 @@ const tokenizeStatic: Tokenizer = function(expr) {
return false; return false;
}; };
const tokenizeOperator: Tokenizer = function(expr) { const tokenizeOperator: Tokenizer = function (expr) {
for (let op of OPERATORS) { for (let op of OPERATORS) {
if (expr.startsWith(op)) { if (expr.startsWith(op)) {
return { type: "OPERATOR", value: op }; return { type: "OPERATOR", value: op };
@@ -167,7 +167,7 @@ const TOKENIZERS = [
tokenizeNumber, tokenizeNumber,
tokenizeOperator, tokenizeOperator,
tokenizeSymbol, tokenizeSymbol,
tokenizeStatic tokenizeStatic,
]; ];
/** /**
@@ -284,6 +284,6 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
export function compileExpr(expr: string, scope: { [key: string]: QWebVar }): string { export function compileExpr(expr: string, scope: { [key: string]: QWebVar }): string {
return compileExprToArray(expr, scope) return compileExprToArray(expr, scope)
.map(t => t.value) .map((t) => t.value)
.join(""); .join("");
} }
+51 -22
View File
@@ -23,7 +23,7 @@ import { QWeb } from "./qweb";
export const MODS_CODE = { export const MODS_CODE = {
prevent: "e.preventDefault();", prevent: "e.preventDefault();",
self: "if (e.target !== this.elm) {return}", self: "if (e.target !== this.elm) {return}",
stop: "e.stopPropagation();" stop: "e.stopPropagation();",
}; };
interface HandlerInfo { interface HandlerInfo {
@@ -50,7 +50,7 @@ export function makeHandlerCode(
let code: string; let code: string;
// check if it is a method with no args, a method with args or an expression // check if it is a method with no args, a method with args or an expression
let args: string = ""; let args: string = "";
const name: string = value.replace(/\(.*\)/, function(_args) { const name: string = value.replace(/\(.*\)/, function (_args) {
args = _args.slice(1, -1); args = _args.slice(1, -1);
return ""; return "";
}); });
@@ -74,7 +74,7 @@ export function makeHandlerCode(
putInCache = false; putInCache = false;
code = ctx.captureExpression(value); code = ctx.captureExpression(value);
} }
const modCode = mods.map(mod => modcodes[mod]).join(""); const modCode = mods.map((mod) => modcodes[mod]).join("");
let handler = `function (e) {if (!context.__owl__.isMounted){return}${modCode}${code}}`; let handler = `function (e) {if (!context.__owl__.isMounted){return}${modCode}${code}}`;
if (putInCache) { if (putInCache) {
const key = ctx.generateTemplateKey(event); const key = ctx.generateTemplateKey(event);
@@ -90,7 +90,7 @@ QWeb.addDirective({
atNodeCreation({ ctx, fullName, value, nodeID }) { atNodeCreation({ ctx, fullName, value, nodeID }) {
const { event, handler } = makeHandlerCode(ctx, fullName, value, true); const { event, handler } = makeHandlerCode(ctx, fullName, value, true);
ctx.addLine(`p${nodeID}.on['${event}'] = ${handler};`); ctx.addLine(`p${nodeID}.on['${event}'] = ${handler};`);
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -105,17 +105,17 @@ QWeb.addDirective({
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`); ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`); addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`); addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`);
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-transition // t-transition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
QWeb.utils.nextFrame = function(cb: () => void) { QWeb.utils.nextFrame = function (cb: () => void) {
requestAnimationFrame(() => requestAnimationFrame(cb)); requestAnimationFrame(() => requestAnimationFrame(cb));
}; };
QWeb.utils.transitionInsert = function(vn: VNode, name: string) { QWeb.utils.transitionInsert = function (vn: VNode, name: string) {
const elm = <HTMLElement>vn.elm; const elm = <HTMLElement>vn.elm;
// remove potential duplicated vnode that is currently being removed, to // remove potential duplicated vnode that is currently being removed, to
// prevent from having twice the same node in the DOM during an animation // prevent from having twice the same node in the DOM during an animation
@@ -139,7 +139,7 @@ QWeb.utils.transitionInsert = function(vn: VNode, name: string) {
}); });
}; };
QWeb.utils.transitionRemove = function(vn: VNode, name: string, rm: () => void) { QWeb.utils.transitionRemove = function (vn: VNode, name: string, rm: () => void) {
const elm = <HTMLElement>vn.elm; const elm = <HTMLElement>vn.elm;
elm.setAttribute("data-owl-key", vn.key!); elm.setAttribute("data-owl-key", vn.key!);
@@ -209,12 +209,12 @@ QWeb.addDirective({
let name = value; let name = value;
const hooks = { const hooks = {
insert: `utils.transitionInsert(vn, '${name}');`, insert: `utils.transitionInsert(vn, '${name}');`,
remove: `utils.transitionRemove(vn, '${name}', rm);` remove: `utils.transitionRemove(vn, '${name}', rm);`,
}; };
for (let hookName in hooks) { for (let hookName in hooks) {
addNodeHook(hookName, hooks[hookName]); addNodeHook(hookName, hooks[hookName]);
} }
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -251,17 +251,20 @@ QWeb.addDirective({
} }
ctx.closeIf(); ctx.closeIf();
return true; return true;
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-model // t-model
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
QWeb.utils.toNumber = function(val: string): number | string { QWeb.utils.toNumber = function (val: string): number | string {
const n = parseFloat(val); const n = parseFloat(val);
return isNaN(n) ? val : n; return isNaN(n) ? val : n;
}; };
const hasDotAtTheEnd = /\.[\w_]+\s*$/;
const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
QWeb.addDirective({ QWeb.addDirective({
name: "model", name: "model",
priority: 42, priority: 42,
@@ -270,15 +273,41 @@ QWeb.addDirective({
let handler; let handler;
let event = fullName.includes(".lazy") ? "change" : "input"; let event = fullName.includes(".lazy") ? "change" : "input";
// we keep here a reference to the "base expression" (if the expression // First step: we need to understand the structure of the expression, and
// is `t-model="some.expr.value", then the base expression is "some.expr"). // from it, extract a base expression (that we can capture, which is
// This is necessary so we can capture it in the handler closure. // important because it will be used in a handler later) and a formatted
let expr = ctx.formatExpression(value); // expression (which uses the captured base expression)
const index = expr.lastIndexOf("."); //
const baseExpr = expr.slice(0, index); // Also, we support 2 kinds of values: some.expr.value or some.expr[value]
ctx.addLine(`let expr${nodeID} = ${baseExpr};`); // For the first one, we have:
// - base expression = scope[some].expr
// - expression = exprX.value (where exprX is the var that captures the base expr)
// and for the expression with brackets:
// - base expression = scope[some].expr
// - expression = exprX[keyX] (where exprX is the var that captures the base expr
// and keyX captures scope[value])
let expr: string;
let baseExpr: string;
if (hasDotAtTheEnd.test(value)) {
// we manage the case where the expr has a dot: some.expr.value
const index = value.lastIndexOf(".");
baseExpr = value.slice(0, index);
ctx.addLine(`let expr${nodeID} = ${ctx.formatExpression(baseExpr)};`);
expr = `expr${nodeID}${value.slice(index)}`;
} else if (hasBracketsAtTheEnd.test(value)) {
// we manage here the case where the expr ends in a bracket expression:
// some.expr[value]
const index = value.lastIndexOf("[");
baseExpr = value.slice(0, index);
ctx.addLine(`let expr${nodeID} = ${ctx.formatExpression(baseExpr)};`);
let exprKey = value.trimRight().slice(index + 1, -1);
ctx.addLine(`let exprKey${nodeID} = ${ctx.formatExpression(exprKey)};`);
expr = `expr${nodeID}[exprKey${nodeID}]`;
} else {
throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
}
expr = `expr${nodeID}.${expr.slice(index + 1)}`;
const key = ctx.generateTemplateKey(); const key = ctx.generateTemplateKey();
if (node.tagName === "select") { if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`); ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
@@ -305,7 +334,7 @@ QWeb.addDirective({
} }
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`); ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`); ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`);
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -329,5 +358,5 @@ QWeb.addDirective({
if (ctx.loopNumber === 0) { if (ctx.loopNumber === 0) {
ctx.rootContext.hasKey0 = ctx.keyStack.pop() as boolean; ctx.rootContext.hasKey0 = ctx.keyStack.pop() as boolean;
} }
} },
}); });
+83 -55
View File
@@ -66,8 +66,6 @@ interface QWebConfig {
// Const/global stuff/helpers // Const/global stuff/helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"]; const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const lineBreakRE = /[\r\n]/; const lineBreakRE = /[\r\n]/;
@@ -77,7 +75,7 @@ const NODE_HOOKS_PARAMS = {
create: "(_, n)", create: "(_, n)",
insert: "vn", insert: "vn",
remove: "(vn, rm)", remove: "(vn, rm)",
destroy: "()" destroy: "()",
}; };
interface Utils { interface Utils {
@@ -86,10 +84,30 @@ interface Utils {
[key: string]: any; [key: string]: any;
} }
function isComponent(obj) { function isComponent(obj): boolean {
return obj && obj.hasOwnProperty("__owl__"); return obj && obj.hasOwnProperty("__owl__");
} }
class VDomArray extends Array {
toString() {
return vDomToString(this);
}
}
function vDomToString(vdom: VNode[]): string {
return vdom
.map((vnode) => {
if (vnode.sel) {
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
} else {
return vnode.text;
}
})
.join("");
}
const UTILS: Utils = { const UTILS: Utils = {
zero: Symbol("zero"), zero: Symbol("zero"),
toObj(expr) { toObj(expr) {
@@ -111,20 +129,8 @@ const UTILS: Utils = {
addNameSpace(vnode) { addNameSpace(vnode) {
addNS(vnode.data, vnode.children, vnode.sel); addNS(vnode.data, vnode.children, vnode.sel);
}, },
VDomArray: class VDomArray extends Array {}, VDomArray,
vDomToString: function(vdom: VNode[]): string { vDomToString,
return vdom
.map(vnode => {
if (vnode.sel) {
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
} else {
return vnode.text;
}
})
.join("");
},
getComponent(obj) { getComponent(obj) {
while (obj && !isComponent(obj)) { while (obj && !isComponent(obj)) {
obj = obj.__proto__; obj = obj.__proto__;
@@ -145,7 +151,7 @@ const UTILS: Utils = {
obj = newObj; obj = newObj;
} }
return obj; return obj;
} },
}; };
function parseXML(xml: string): Document { function parseXML(xml: string): Document {
@@ -195,7 +201,7 @@ export class QWeb extends EventBus {
name: 1, name: 1,
att: 1, att: 1,
attf: 1, attf: 1,
translation: 1 translation: 1,
}; };
static DIRECTIVES: Directive[] = []; static DIRECTIVES: Directive[] = [];
@@ -212,10 +218,12 @@ export class QWeb extends EventBus {
static slots = {}; static slots = {};
static nextSlotId = 1; static nextSlotId = 1;
// recursiveTemplates contains sub templates called with t-call, but which // subTemplates are stored in two objects: a (local) mapping from a name to an
// ends up in recursive situations. This is very similar to the slot situation, // id, and a (global) mapping from an id to the compiled function. This is
// as in we need to propagate the scope. // necessary to ensure that global templates can be called with more than one
subTemplates = {}; // QWeb instance.
subTemplates: { [key: string]: number } = {};
static subTemplates: { [id: number]: Function } = {};
isUpdating: boolean = false; isUpdating: boolean = false;
translateFn?: QWebConfig["translateFn"]; translateFn?: QWebConfig["translateFn"];
@@ -239,7 +247,7 @@ export class QWeb extends EventBus {
QWeb.DIRECTIVE_NAMES[directive.name] = 1; QWeb.DIRECTIVE_NAMES[directive.name] = 1;
QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority); QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
if (directive.extraNames) { if (directive.extraNames) {
directive.extraNames.forEach(n => (QWeb.DIRECTIVE_NAMES[n] = 1)); directive.extraNames.forEach((n) => (QWeb.DIRECTIVE_NAMES[n] = 1));
} }
} }
@@ -303,11 +311,11 @@ export class QWeb extends EventBus {
this._processTemplate(elem); this._processTemplate(elem);
const template = { const template = {
elem, elem,
fn: function(this: QWeb, context, extra) { fn: function (this: QWeb, context, extra) {
const compiledFunction = this._compile(name, elem); const compiledFunction = this._compile(name, elem);
template.fn = compiledFunction; template.fn = compiledFunction;
return compiledFunction.call(this, context, extra); return compiledFunction.call(this, context, extra);
} },
}; };
this.templates[name] = template; this.templates[name] = template;
} }
@@ -317,10 +325,10 @@ export class QWeb extends EventBus {
for (let i = 0, ilen = tbranch.length; i < ilen; i++) { for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
let node = tbranch[i]; let node = tbranch[i];
let prevElem = node.previousElementSibling!; let prevElem = node.previousElementSibling!;
let pattr = function(name) { let pattr = function (name) {
return prevElem.getAttribute(name); return prevElem.getAttribute(name);
}; };
let nattr = function(name) { let nattr = function (name) {
return +!!node.getAttribute(name); return +!!node.getAttribute(name);
}; };
if (prevElem && (pattr("t-if") || pattr("t-elif"))) { if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
@@ -330,7 +338,7 @@ export class QWeb extends EventBus {
); );
} }
if ( if (
["t-if", "t-elif", "t-else"].map(nattr).reduce(function(a, b) { ["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
return a + b; return a + b;
}) > 1 }) > 1
) { ) {
@@ -507,10 +515,17 @@ export class QWeb extends EventBus {
return; return;
} }
if (node.tagName !== "t" && node.hasAttribute("t-call")) {
const tCallNode = document.createElement("t");
tCallNode.setAttribute("t-call", node.getAttribute("t-call")!);
node.removeAttribute("t-call");
node.prepend(tCallNode);
}
const firstLetter = node.tagName[0]; const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) { if (firstLetter === firstLetter.toUpperCase()) {
// this is a component, we modify in place the xml document to change // this is a component, we modify in place the xml document to change
// <SomeComponent ... /> to <t t-component="SomeComponent" ... /> // <SomeComponent ... /> to <SomeComponent t-component="SomeComponent" ... />
node.setAttribute("t-component", node.tagName); node.setAttribute("t-component", node.tagName);
} else if (node.tagName !== "t" && node.hasAttribute("t-component")) { } else if (node.tagName !== "t" && node.hasAttribute("t-component")) {
throw new Error( throw new Error(
@@ -582,7 +597,7 @@ export class QWeb extends EventBus {
qweb: this, qweb: this,
ctx, ctx,
fullName, fullName,
value value,
}); });
if (isDone) { if (isDone) {
for (let { directive, value, fullName } of finalizers) { for (let { directive, value, fullName } of finalizers) {
@@ -597,7 +612,7 @@ export class QWeb extends EventBus {
let nodeID = this._compileGenericNode(node, ctx, withHandlers); let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID); ctx = ctx.withParent(nodeID);
let nodeHooks = {}; let nodeHooks = {};
let addNodeHook = function(hook, handler) { let addNodeHook = function (hook, handler) {
nodeHooks[hook] = nodeHooks[hook] || []; nodeHooks[hook] = nodeHooks[hook] || [];
nodeHooks[hook].push(handler); nodeHooks[hook].push(handler);
}; };
@@ -611,7 +626,7 @@ export class QWeb extends EventBus {
fullName, fullName,
value, value,
nodeID, nodeID,
addNodeHook addNodeHook,
}); });
} }
} }
@@ -664,22 +679,31 @@ export class QWeb extends EventBus {
const props: string[] = []; const props: string[] = [];
const tattrs: number[] = []; const tattrs: number[] = [];
function handleBooleanProps(key, val) { function handleProperties(key, val) {
let isProp = false; let isProp = false;
if (node.nodeName === "input" && key === "checked") { switch (node.nodeName) {
let type = (<Element>node).getAttribute("type"); case "input":
if (type === "checkbox" || type === "radio") { let type = (<Element>node).getAttribute("type");
isProp = true; if (type === "checkbox" || type === "radio") {
} if (key === "checked" || key === "indeterminate") {
} isProp = true;
if (node.nodeName === "option" && key === "selected") { }
isProp = true; }
} if (key === "value" || key === "readonly" || key === "disabled") {
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) { isProp = true;
isProp = true; }
} break;
if ((key === "readonly" && node.nodeName === "input") || node.nodeName === "textarea") { case "option":
isProp = true; isProp = key === "selected" || key === "disabled";
break;
case "textarea":
isProp = key === "readonly" || key === "disabled";
break;
case "button":
case "select":
case "optgroup":
isProp = key === "disabled";
break;
} }
if (isProp) { if (isProp) {
props.push(`${key}: _${val}`); props.push(`${key}: _${val}`);
@@ -702,10 +726,14 @@ export class QWeb extends EventBus {
if ((value = value.trim())) { if ((value = value.trim())) {
let classDef = value let classDef = value
.split(/\s+/) .split(/\s+/)
.map(a => `'${escapeQuotes(a)}':true`) .map((a) => `'${escapeQuotes(a)}':true`)
.join(","); .join(",");
classObj = `_${ctx.generateID()}`; if (classObj) {
ctx.addLine(`let ${classObj} = {${classDef}};`); ctx.addLine(`Object.assign(${classObj}, {${classDef}})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
} }
} else { } else {
ctx.addLine(`let _${attID} = '${escapeQuotes(value)}';`); ctx.addLine(`let _${attID} = '${escapeQuotes(value)}';`);
@@ -714,7 +742,7 @@ export class QWeb extends EventBus {
name = '"' + name + '"'; name = '"' + name + '"';
} }
attrs.push(`${name}: _${attID}`); attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID); handleProperties(name, attID);
} }
} }
@@ -746,12 +774,12 @@ export class QWeb extends EventBus {
const attValueID = ctx.generateID(); const attValueID = ctx.generateID();
ctx.addLine(`let _${attValueID} = ${formattedValue};`); ctx.addLine(`let _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`; formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":")); const attrIndex = attrs.findIndex((att) => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1); attrs.splice(attrIndex, 1);
} }
ctx.addLine(`let _${attID} = ${formattedValue};`); ctx.addLine(`let _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`); attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID); handleProperties(attName, attID);
} }
} }
+3 -3
View File
@@ -97,7 +97,7 @@ export class Router {
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
async start() { async start() {
(this as any)._listener = ev => this._navigate(this.currentPath(), ev); (this as any)._listener = (ev) => this._navigate(this.currentPath(), ev);
window.addEventListener("popstate", (this as any)._listener); window.addEventListener("popstate", (this as any)._listener);
if (this.mode === "hash") { if (this.mode === "hash") {
window.addEventListener("hashchange", (this as any)._listener); window.addEventListener("hashchange", (this as any)._listener);
@@ -198,7 +198,7 @@ export class Router {
return { return {
type: "match", type: "match",
route: route, route: route,
params: params params: params,
}; };
} }
} }
@@ -223,7 +223,7 @@ export class Router {
const result = await route.beforeRouteEnter({ const result = await route.beforeRouteEnter({
env: this.env, env: this.env,
from: this.currentRoute, from: this.currentRoute,
to: route to: route,
}); });
if (result === false) { if (result === false) {
return { type: "cancelled" }; return { type: "cancelled" };
+7 -7
View File
@@ -53,7 +53,7 @@ export class Store extends Context {
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);
@@ -70,7 +70,7 @@ export class Store extends Context {
dispatch: this.dispatch.bind(this), dispatch: this.dispatch.bind(this),
env: this.env, env: this.env,
state: this.state, state: this.state,
getters: this.getters getters: this.getters,
}, },
...payload ...payload
); );
@@ -113,11 +113,11 @@ export function useStore(selector, options: SelectorOptions = {}): any {
} }
return false; return false;
} }
store.updateFunctions[componentId].push(function(): boolean { store.updateFunctions[componentId].push(function (): boolean {
return selectCompareUpdate(store!.state, component.props); return selectCompareUpdate(store!.state, component.props);
}); });
useContextWithCB(store, component, function(): Promise<void> | void { useContextWithCB(store, component, function (): Promise<void> | void {
let shouldRender = false; let shouldRender = false;
for (let fn of store.updateFunctions[componentId]) { for (let fn of store.updateFunctions[componentId]) {
shouldRender = fn() || shouldRender; shouldRender = fn() || shouldRender;
@@ -126,12 +126,12 @@ export function useStore(selector, options: SelectorOptions = {}): any {
return component.render(); return component.render();
} }
}); });
onWillUpdateProps(props => { onWillUpdateProps((props) => {
selectCompareUpdate(store.state, props); selectCompareUpdate(store.state, props);
}); });
const __destroy = component.__destroy; const __destroy = component.__destroy;
component.__destroy = parent => { component.__destroy = (parent) => {
delete store.updateFunctions[componentId]; delete store.updateFunctions[componentId];
__destroy.call(component, parent); __destroy.call(component, parent);
}; };
@@ -148,7 +148,7 @@ export function useStore(selector, options: SelectorOptions = {}): any {
}, },
has(target, k) { has(target, k) {
return k in result; return k in result;
} },
}); });
} }
+6 -6
View File
@@ -13,13 +13,13 @@
import { browser } from "./browser"; import { browser } from "./browser";
export function whenReady(fn?: any) { export function whenReady(fn?: any) {
return new Promise(function(resolve) { return new Promise(function (resolve) {
if (document.readyState !== "loading") { if (document.readyState !== "loading") {
resolve(); resolve();
} else { } else {
document.addEventListener("DOMContentLoaded", resolve, false); document.addEventListener("DOMContentLoaded", resolve, false);
} }
}).then(fn || function() {}); }).then(fn || function () {});
} }
const loadedScripts: { [key: string]: Promise<void> } = {}; const loadedScripts: { [key: string]: Promise<void> } = {};
@@ -28,14 +28,14 @@ export function loadJS(url: string): Promise<void> {
if (url in loadedScripts) { if (url in loadedScripts) {
return loadedScripts[url]; return loadedScripts[url];
} }
const promise: Promise<void> = new Promise(function(resolve, reject) { const promise: Promise<void> = new Promise(function (resolve, reject) {
const script = document.createElement("script"); const script = document.createElement("script");
script.type = "text/javascript"; script.type = "text/javascript";
script.src = url; script.src = url;
script.onload = function() { script.onload = function () {
resolve(); resolve();
}; };
script.onerror = function() { script.onerror = function () {
reject(`Error loading file '${url}'`); reject(`Error loading file '${url}'`);
}; };
const head = document.head || document.getElementsByTagName("head")[0]; const head = document.head || document.getElementsByTagName("head")[0];
@@ -75,7 +75,7 @@ export function escape(str: string | number | undefined): string {
*/ */
export function debounce(func: Function, wait: number, immediate?: boolean): Function { export function debounce(func: Function, wait: number, immediate?: boolean): Function {
let timeout; let timeout;
return function(this: any) { return function (this: any) {
const context = this; const context = this;
const args = arguments; const args = arguments;
function later() { function later() {
+6 -2
View File
@@ -1,4 +1,4 @@
import { VNode, h } from "./vdom"; import { VNode, h, addNS } from "./vdom";
const parser = new DOMParser(); const parser = new DOMParser();
@@ -23,5 +23,9 @@ function htmlToVNode(node: ChildNode): VNode {
for (let c of node.childNodes) { for (let c of node.childNodes) {
children.push(htmlToVNode(c)); children.push(htmlToVNode(c));
} }
return h((node as Element).tagName, { attrs }, children); const vnode = h((node as Element).tagName, { attrs }, children);
if (vnode.sel === "svg") {
addNS(vnode.data, (vnode as any).children, vnode.sel);
}
return vnode;
} }
+3 -3
View File
@@ -33,7 +33,7 @@ function updateProps(oldVnode: VNode, vnode: VNode): void {
export const propsModule = { export const propsModule = {
create: updateProps, create: updateProps,
update: updateProps update: updateProps,
} as Module; } as Module;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -151,7 +151,7 @@ function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
export const eventListenersModule = { export const eventListenersModule = {
create: updateEventListeners, create: updateEventListeners,
update: updateEventListeners, update: updateEventListeners,
destroy: updateEventListeners destroy: updateEventListeners,
} as Module; } as Module;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -210,7 +210,7 @@ function updateAttrs(oldVnode: VNode, vnode: VNode): void {
export const attrsModule = { export const attrsModule = {
create: updateAttrs, create: updateAttrs,
update: updateAttrs update: updateAttrs,
} as Module; } as Module;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
+1 -1
View File
@@ -520,7 +520,7 @@ const htmlDomApi = {
parentNode, parentNode,
nextSibling, nextSibling,
tagName, tagName,
setTextContent setTextContent,
} as DOMAPI; } as DOMAPI;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
+13 -13
View File
@@ -9,7 +9,7 @@ import {
patchNextFrame, patchNextFrame,
renderToDOM, renderToDOM,
unpatchNextFrame, unpatchNextFrame,
nextTick nextTick,
} from "./helpers"; } from "./helpers";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -71,7 +71,7 @@ describe("animations", () => {
qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`); qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`);
let def = makeDeferred(); let def = makeDeferred();
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(node.className).toBe("chimay-enter chimay-enter-active"); expect(node.className).toBe("chimay-enter chimay-enter-active");
cb(); cb();
expect(node.className).toBe("chimay-enter-active chimay-enter-to"); expect(node.className).toBe("chimay-enter-active chimay-enter-to");
@@ -91,7 +91,7 @@ describe("animations", () => {
qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`); qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`);
let def = makeDeferred(); let def = makeDeferred();
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(node.className).toBe("chimay-enter chimay-enter-active"); expect(node.className).toBe("chimay-enter chimay-enter-active");
cb(); cb();
expect(node.className).toBe("chimay-enter-active chimay-enter-to"); expect(node.className).toBe("chimay-enter-active chimay-enter-to");
@@ -112,7 +112,7 @@ describe("animations", () => {
qweb.addTemplate("test", `<span t-transition="jupiler">blue</span>`); qweb.addTemplate("test", `<span t-transition="jupiler">blue</span>`);
let def = makeDeferred(); let def = makeDeferred();
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(node.className).toBe("jupiler-enter jupiler-enter-active"); expect(node.className).toBe("jupiler-enter jupiler-enter-active");
cb(); cb();
expect(node.className).toBe(""); expect(node.className).toBe("");
@@ -139,7 +139,7 @@ describe("animations", () => {
// insert widget into the DOM // insert widget into the DOM
let def = makeDeferred(); let def = makeDeferred();
var spanNode; var spanNode;
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(spanNode.className).toBe("chimay-enter chimay-enter-active"); expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
cb(); cb();
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to"); expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
@@ -155,7 +155,7 @@ describe("animations", () => {
// remove span from the DOM // remove span from the DOM
def = makeDeferred(); def = makeDeferred();
widget.state.hide = true; widget.state.hide = true;
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(spanNode.className).toBe("chimay-leave chimay-leave-active"); expect(spanNode.className).toBe("chimay-leave chimay-leave-active");
cb(); cb();
expect(spanNode.className).toBe("chimay-leave-active chimay-leave-to"); expect(spanNode.className).toBe("chimay-leave-active chimay-leave-to");
@@ -182,7 +182,7 @@ describe("animations", () => {
// insert widget into the DOM // insert widget into the DOM
let def = makeDeferred(); let def = makeDeferred();
var spanNode; var spanNode;
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(spanNode.className).toBe("chimay-enter chimay-enter-active"); expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
cb(); cb();
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to"); expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
@@ -210,7 +210,7 @@ describe("animations", () => {
let def = makeDeferred(); let def = makeDeferred();
var spanNode; var spanNode;
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>' '<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
); );
@@ -250,7 +250,7 @@ describe("animations", () => {
let def = makeDeferred(); let def = makeDeferred();
var spanNode; var spanNode;
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>' '<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
); );
@@ -275,7 +275,7 @@ describe("animations", () => {
// remove span from the DOM // remove span from the DOM
def = makeDeferred(); def = makeDeferred();
widget.state.display = false; widget.state.display = false;
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="__3__">blue</span></div>' '<div><span class="chimay-leave chimay-leave-active" data-owl-key="__3__">blue</span></div>'
); );
@@ -315,7 +315,7 @@ describe("animations", () => {
let def = makeDeferred(); let def = makeDeferred();
let phase = "enter"; let phase = "enter";
patchNextFrame(cb => { patchNextFrame((cb) => {
let spans = fixture.querySelectorAll("span"); let spans = fixture.querySelectorAll("span");
expect(spans.length).toBe(1); expect(spans.length).toBe(1);
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`); expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
@@ -366,7 +366,7 @@ describe("animations", () => {
let def = makeDeferred(); let def = makeDeferred();
let phase = "enter"; let phase = "enter";
patchNextFrame(cb => { patchNextFrame((cb) => {
let spans = fixture.querySelectorAll("span"); let spans = fixture.querySelectorAll("span");
expect(spans.length).toBe(1); expect(spans.length).toBe(1);
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`); expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
@@ -413,7 +413,7 @@ describe("animations", () => {
state = useState({ flag: false }); state = useState({ flag: false });
} }
patchNextFrame(cb => cb()); patchNextFrame((cb) => cb());
const widget = new Parent(); const widget = new Parent();
await widget.mount(fixture); await widget.mount(fixture);
@@ -1253,12 +1253,16 @@ exports[`other directives with t-component t-set can't alter from within callee
if (scope.iter != null) { if (scope.iter != null) {
c2.push({text: scope.iter}); c2.push({text: scope.iter});
} }
this.subTemplates['ChildWidget'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'})); let _origScope4 = scope;
let c5 = [], p5 = {key:5}; scope = Object.create(scope);
let vn5 = h('p', p5, c5); scope.__access_mode__ = 'ro';
c1.push(vn5); this.constructor.subTemplates['2'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'}));
scope = _origScope4;
let c6 = [], p6 = {key:6};
let vn6 = h('p', p6, c6);
c1.push(vn6);
if (scope.iter != null) { if (scope.iter != null) {
c5.push({text: scope.iter}); c6.push({text: scope.iter});
} }
return vn1; return vn1;
}" }"
@@ -1280,18 +1284,18 @@ exports[`other directives with t-component t-set can't alter in t-call body 1`]
if (scope.iter != null) { if (scope.iter != null) {
c2.push({text: scope.iter}); c2.push({text: scope.iter});
} }
let _origScope4 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope4 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
utils.getScope(scope, 'iter').iter = 'inCall'; utils.getScope(scope, 'iter').iter = 'inCall';
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
this.subTemplates['ChildWidget'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'})); this.constructor.subTemplates['2'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'}));
scope = _origScope4;
} }
scope = _origScope4;
let c6 = [], p6 = {key:6}; let c6 = [], p6 = {key:6};
let vn6 = h('p', p6, c6); let vn6 = h('p', p6, c6);
c1.push(vn6); c1.push(vn6);
@@ -1401,6 +1405,47 @@ exports[`other directives with t-component t-set outside modified in t-foreach 1
}" }"
`; `;
exports[`props evaluation t-set with a body expression can be used as textual prop 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let c2 = new utils.VDomArray();
c2.push({text: \`42\`});
scope.abc = c2
// Component 'Child'
let w3 = '__4__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__4__']] : false;
let props3 = {val:scope.abc};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined);
let pvnode = w3.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| scope['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap['__4__'] = w3.__owl__.id;
let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__4__', hook: {remove() {},destroy(vn) {w3.destroy();}}});
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
w3.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`random stuff/miscellaneous can inject values in tagged templates 1`] = ` exports[`random stuff/miscellaneous can inject values in tagged templates 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -1410,7 +1455,11 @@ exports[`random stuff/miscellaneous can inject values in tagged templates 1`] =
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
this.subTemplates['__template__1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'})); let _origScope4 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['3'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'}));
scope = _origScope4;
return vn1; return vn1;
}" }"
`; `;
@@ -1683,6 +1732,33 @@ exports[`t-model directive basic use, on an input 1`] = `
}" }"
`; `;
exports[`t-model directive basic use, on an input with bracket expression 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('input', p2, c2);
c1.push(vn2);
let expr2 = scope['state'];
let exprKey2 = 'text';
p2.props = {value: expr2[exprKey2]};
extra.handlers['__3__'] = extra.handlers['__3__'] || ((ev) => {expr2[exprKey2] = ev.target.value});
p2.on['input'] = extra.handlers['__3__'];
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
let _5 = scope['state'].text;
if (_5 != null) {
c4.push({text: _5});
}
return vn1;
}"
`;
exports[`t-model directive basic use, on another key in component 1`] = ` exports[`t-model directive basic use, on another key in component 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -1749,6 +1825,46 @@ exports[`t-model directive in a t-foreach 1`] = `
}" }"
`; `;
exports[`t-model directive in a t-foreach, part 2 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let _2 = scope['state'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
}
let _length3 = _3.length;
let _origScope5 = scope;
scope = Object.create(scope);
for (let i1 = 0; i1 < _length3; i1++) {
scope.thing_first = i1 === 0
scope.thing_last = i1 === _length3 - 1
scope.thing_index = i1
scope.thing = _3[i1]
scope.thing_value = _4[i1]
let key1 = scope['thing_index'];
let c6 = [], p6 = {key:\`\${key1}_6\`,on:{}};
let vn6 = h('input', p6, c6);
c1.push(vn6);
let expr6 = scope['state'];
let exprKey6 = scope['thing_index'];
let k7 = \`__7__\${key1}__\`;
p6.props = {value: expr6[exprKey6]};
extra.handlers[k7] = extra.handlers[k7] || ((ev) => {expr6[exprKey6] = ev.target.value});
p6.on['input'] = extra.handlers[k7];
}
scope = _origScope5;
return vn1;
}"
`;
exports[`t-model directive on a select 1`] = ` exports[`t-model directive on a select 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -1833,7 +1949,7 @@ exports[`t-model directive on an input type=radio 1`] = `
let _2 = 'radio'; let _2 = 'radio';
let _3 = 'one'; let _3 = 'one';
let _4 = 'One'; let _4 = 'One';
let c5 = [], p5 = {key:5,attrs:{type: _2,id: _3,value: _4},on:{}}; let c5 = [], p5 = {key:5,attrs:{type: _2,id: _3,value: _4},props:{value: _4},on:{}};
let vn5 = h('input', p5, c5); let vn5 = h('input', p5, c5);
c1.push(vn5); c1.push(vn5);
let expr5 = scope['state']; let expr5 = scope['state'];
@@ -1843,7 +1959,7 @@ exports[`t-model directive on an input type=radio 1`] = `
let _7 = 'radio'; let _7 = 'radio';
let _8 = 'two'; let _8 = 'two';
let _9 = 'Two'; let _9 = 'Two';
let c10 = [], p10 = {key:10,attrs:{type: _7,id: _8,value: _9},on:{}}; let c10 = [], p10 = {key:10,attrs:{type: _7,id: _8,value: _9},props:{value: _9},on:{}};
let vn10 = h('input', p10, c10); let vn10 = h('input', p10, c10);
c1.push(vn10); c1.push(vn10);
let expr10 = scope['state']; let expr10 = scope['state'];
+2 -2
View File
@@ -27,7 +27,7 @@ afterEach(() => {
function children(w: Component): Component[] { function children(w: Component): Component[] {
const childrenMap = w.__owl__.children; const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]); return Object.keys(childrenMap).map((id) => childrenMap[id]);
} }
describe("async rendering", () => { describe("async rendering", () => {
@@ -404,7 +404,7 @@ describe("async rendering", () => {
} }
class ChildB extends Component { class ChildB extends Component {
willStart(): any { willStart(): any {
return new Promise(function() {}); return new Promise(function () {});
} }
} }
class Parent extends Component { class Parent extends Component {
+223 -32
View File
@@ -1,16 +1,16 @@
import { Component, Env } from "../../src/component/component"; import { Component, Env } from "../../src/component/component";
import { EventBus } from "../../src/core/event_bus";
import { useRef, useState } from "../../src/hooks";
import { QWeb } from "../../src/qweb/qweb"; import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
import { useState, useRef } from "../../src/hooks";
import { EventBus } from "../../src/core/event_bus";
import { import {
editInput,
makeDeferred, makeDeferred,
makeTestFixture,
makeTestEnv, makeTestEnv,
makeTestFixture,
nextMicroTick, nextMicroTick,
nextTick, nextTick,
normalize, normalize,
editInput
} from "../helpers"; } from "../helpers";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -39,7 +39,7 @@ afterEach(() => {
function children(w: Component): Component[] { function children(w: Component): Component[] {
const childrenMap = w.__owl__.children; const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]); return Object.keys(childrenMap).map((id) => childrenMap[id]);
} }
// Test components // Test components
@@ -133,7 +133,7 @@ describe("basic widget properties", () => {
static template = xml` static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`; <div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
state = useState({ state = useState({
counter: 0 counter: 0,
}); });
} }
@@ -167,7 +167,7 @@ describe("basic widget properties", () => {
static template = xml` static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`; <div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
state = useState({ state = useState({
counter: 0 counter: 0,
}); });
} }
@@ -537,7 +537,7 @@ describe("lifecycle hooks", () => {
"child:mounted", "child:mounted",
"parent:willUnmount", "parent:willUnmount",
"child:willUnmount", "child:willUnmount",
"childchild:willUnmount" "childchild:willUnmount",
]); ]);
}); });
@@ -589,7 +589,7 @@ describe("lifecycle hooks", () => {
"childchild:willPatch", "childchild:willPatch",
"childchild:patched", "childchild:patched",
"child:patched", "child:patched",
"parent:patched" "parent:patched",
]); ]);
}); });
@@ -782,7 +782,7 @@ describe("lifecycle hooks", () => {
"c mounted", "c mounted",
"p mounted", "p mounted",
"p willunmount", "p willunmount",
"c willunmount" "c willunmount",
]); ]);
}); });
@@ -960,7 +960,7 @@ describe("lifecycle hooks", () => {
"parent:willPatch", "parent:willPatch",
"child:willPatch", "child:willPatch",
"child:patched", "child:patched",
"parent:patched" "parent:patched",
]); ]);
}); });
}); });
@@ -1134,7 +1134,7 @@ describe("composition", () => {
class App extends Component { class App extends Component {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`; static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = useState({ state = useState({
child: "a" child: "a",
}); });
get myComponent() { get myComponent() {
return this.state.child === "a" ? A : B; return this.state.child === "a" ? A : B;
@@ -1158,7 +1158,7 @@ describe("composition", () => {
class App extends Component { class App extends Component {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`; static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = useState({ state = useState({
child: "a" child: "a",
}); });
get myComponent() { get myComponent() {
return this.state.child === "a" ? A : B; return this.state.child === "a" ? A : B;
@@ -1228,7 +1228,7 @@ describe("composition", () => {
static template = xml` static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`; <div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
state = useState({ state = useState({
counter: 0 counter: 0,
}); });
} }
@@ -1327,7 +1327,7 @@ describe("composition", () => {
static template = xml` static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`; <div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
state = useState({ state = useState({
counter: 0 counter: 0,
}); });
} }
@@ -1350,7 +1350,7 @@ describe("composition", () => {
static template = xml` static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`; <div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
state = useState({ state = useState({
counter: 0 counter: 0,
}); });
} }
@@ -1389,7 +1389,7 @@ describe("composition", () => {
); );
class Parent extends Component { class Parent extends Component {
state = useState({ state = useState({
numbers: [1, 2, 3] numbers: [1, 2, 3],
}); });
static components = { ChildWidget }; static components = { ChildWidget };
} }
@@ -1429,7 +1429,7 @@ describe("composition", () => {
class Parent extends Component { class Parent extends Component {
static template = "parent"; static template = "parent";
state = useState({ state = useState({
numbers: [1, 2, 3] numbers: [1, 2, 3],
}); });
static components = { ChildWidget }; static components = { ChildWidget };
} }
@@ -1513,8 +1513,8 @@ describe("composition", () => {
blips: [ blips: [
{ a: "a", id: 1 }, { a: "a", id: 1 },
{ b: "b", id: 2 }, { b: "b", id: 2 },
{ c: "c", id: 4 } { c: "c", id: 4 },
] ],
}); });
} }
const parent = new Parent(); const parent = new Parent();
@@ -1621,8 +1621,8 @@ describe("composition", () => {
records: [ records: [
{ id: 1, val: 1 }, { id: 1, val: 1 },
{ id: 2, val: 2 }, { id: 2, val: 2 },
{ id: 3, val: 3 } { id: 3, val: 3 },
] ],
}); });
static components = { ChildWidget }; static components = { ChildWidget };
} }
@@ -1725,6 +1725,47 @@ describe("props evaluation ", () => {
await widget.mount(fixture); await widget.mount(fixture);
expect(normalize(fixture.innerHTML)).toBe("<div><span>42</span></div>"); expect(normalize(fixture.innerHTML)).toBe("<div><span>42</span></div>");
}); });
test("t-set with a body expression can be used as textual prop", async () => {
class Child extends Component {
static template = xml`<span t-esc="props.val"/>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
<t t-set="abc">42</t>
<Child val="abc"/>
</div>`;
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
});
test("t-set with a body expression can be passed in props, and then t-raw", async () => {
class Child extends Component {
static template = xml`
<span>
<t t-esc="props.val"/>
<t t-raw="props.val"/>
</span>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
<t t-set="abc"><p>43</p></t>
<Child val="abc"/>
</div>`;
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>&lt;p&gt;4343&lt;/p&gt;<p>43</p></span></div>");
});
}); });
describe("other directives with t-component", () => { describe("other directives with t-component", () => {
@@ -2003,7 +2044,7 @@ describe("other directives with t-component", () => {
} }
const widget = new ParentWidget(); const widget = new ParentWidget();
await widget.mount(fixture); await widget.mount(fixture);
(<HTMLElement>fixture).addEventListener("ev", function(e) { (<HTMLElement>fixture).addEventListener("ev", function (e) {
steps.push(e.defaultPrevented); steps.push(e.defaultPrevented);
}); });
@@ -2038,7 +2079,7 @@ describe("other directives with t-component", () => {
} }
const widget = new ParentWidget(); const widget = new ParentWidget();
await widget.mount(fixture); await widget.mount(fixture);
(<HTMLElement>fixture).addEventListener("ev", function(e) { (<HTMLElement>fixture).addEventListener("ev", function (e) {
steps.push(e.defaultPrevented); steps.push(e.defaultPrevented);
}); });
@@ -2976,7 +3017,7 @@ describe("random stuff/miscellaneous", () => {
"D:mounted", "D:mounted",
"C:mounted", "C:mounted",
"B:mounted", "B:mounted",
"A:mounted" "A:mounted",
]); ]);
// update // update
@@ -2999,7 +3040,7 @@ describe("random stuff/miscellaneous", () => {
"E:destroy", "E:destroy",
"F:mounted", "F:mounted",
"D:patched", "D:patched",
"C:patched" "C:patched",
]); ]);
}); });
@@ -3015,6 +3056,85 @@ describe("random stuff/miscellaneous", () => {
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe("<div><span>42</span></div>"); expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
}); });
test("update props of component without concrete own node", async () => {
class Custom extends Component {
static template = xml`
<div class="widget-subkey">
<t t-esc="props.key"/>__<t t-esc="props.subKey"/>
</div>`;
}
class Child extends Component {
static components = { Custom };
static template = xml`
<t t-component="Custom"
t-key="props.subKey"
key="props.key"
subKey="props.subKey"/>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
<Child t-key="childProps.key" t-props="childProps"/>
</div>`;
childProps = {
key: 1,
subKey: 1,
};
}
const parent = new Parent(null);
await parent.mount(fixture);
expect(fixture.textContent!.trim()).toBe("1__1");
// First step: change the Custom's instance
Object.assign(parent.childProps, {
subKey: 2,
});
parent.render();
await nextTick();
expect(fixture.textContent!.trim()).toBe("1__2");
// Second step, change both Child's and Custom's instance
Object.assign(parent.childProps, {
key: 2,
subKey: 3,
});
parent.render();
await nextTick();
expect(fixture.textContent!.trim()).toBe("2__3");
});
test("two renderings initiated between willPatch and patched", async () => {
let app;
class Panel extends Component {
static template = xml`<abc><t t-esc="props.val"/></abc>`;
mounted() {
app.render();
}
willUnmount() {
app.render();
}
}
// Main root component
class App extends Component {
static components = { Panel };
static template = xml`<div><Panel t-key="'panel_' + state.panel" val="state.panel"/></div>`;
state = useState({ panel: "Panel1" });
}
app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><abc>Panel1</abc></div>");
app.state.panel = "Panel2";
await nextTick();
expect(fixture.innerHTML).toBe("<div><abc>Panel2</abc></div>");
});
}); });
describe("widget and observable state", () => { describe("widget and observable state", () => {
@@ -3138,6 +3258,46 @@ describe("t-model directive", () => {
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
}); });
test("basic use, on an input with bracket expression", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state['text']"/>
<span><t t-esc="state.text"/></span>
</div>`;
state = useState({ text: "" });
}
const comp = new SomeComponent();
await comp.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
const input = fixture.querySelector("input")!;
await editInput(input, "test");
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
});
test("throws if invalid expression", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state"/>
</div>`;
state = useState({ text: "" });
}
const comp = new SomeComponent();
let error;
try {
await comp.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Invalid t-model expression: "state" (it should be assignable)`);
});
test("basic use, on another key in component", async () => { test("basic use, on another key in component", async () => {
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
@@ -3410,7 +3570,7 @@ describe("t-model directive", () => {
state = useState([ state = useState([
{ f: false, id: 1 }, { f: false, id: 1 },
{ f: false, id: 2 }, { f: false, id: 2 },
{ f: false, id: 3 } { f: false, id: 3 },
]); ]);
} }
const comp = new SomeComponent(); const comp = new SomeComponent();
@@ -3427,6 +3587,28 @@ describe("t-model directive", () => {
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
}); });
test("in a t-foreach, part 2", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<t t-foreach="state" t-as="thing" t-key="thing_index" >
<input t-model="state[thing_index]"/>
</t>
</div>
`;
state = useState(["zuko", "iroh"]);
}
const comp = new SomeComponent();
await comp.mount(fixture);
expect(comp.state).toEqual(["zuko", "iroh"]);
const input = fixture.querySelectorAll("input")[1]!;
input.value = "uncle iroh";
input.dispatchEvent(new Event("input"));
expect(comp.state).toEqual(["zuko", "uncle iroh"]);
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
});
test("two inputs in a div with a t-key", async () => { test("two inputs in a div with a t-key", async () => {
class SomeComponent extends Component { class SomeComponent extends Component {
static template = xml` static template = xml`
@@ -3455,7 +3637,7 @@ describe("environment and plugins", () => {
let bus = new EventBus(); let bus = new EventBus();
// definition of a plugin // definition of a plugin
const somePlugin = env => { const somePlugin = (env) => {
env.someFlag = true; env.someFlag = true;
bus.on("some-event", null, () => { bus.on("some-event", null, () => {
env.someFlag = !env.someFlag; env.someFlag = !env.someFlag;
@@ -3825,7 +4007,9 @@ describe("t-call", () => {
expect(fixture.innerHTML).toBe("<div><p>lucas</p></div>"); expect(fixture.innerHTML).toBe("<div><p>lucas</p></div>");
fixture.querySelector("p")!.click(); fixture.querySelector("p")!.click();
expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot(); const subId = env.qweb.subTemplates["sub"];
const subFn = QWeb.subTemplates[subId] as any;
expect(subFn.toString()).toMatchSnapshot();
}); });
test("parent is set within t-call", async () => { test("parent is set within t-call", async () => {
@@ -3847,7 +4031,10 @@ describe("t-call", () => {
expect(fixture.innerHTML).toBe("<div><span>lucas</span></div>"); expect(fixture.innerHTML).toBe("<div><span>lucas</span></div>");
expect(child.__owl__.parent).toBe(parent); expect(child.__owl__.parent).toBe(parent);
expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot(); const subId = env.qweb.subTemplates["sub"];
const subFn = QWeb.subTemplates[subId] as any;
expect(subFn.toString()).toMatchSnapshot();
}); });
test("t-call in t-foreach and children component", async () => { test("t-call in t-foreach and children component", async () => {
@@ -3889,7 +4076,9 @@ describe("t-call", () => {
expect(fixture.innerHTML).toBe("<span>lucas</span>"); expect(fixture.innerHTML).toBe("<span>lucas</span>");
expect(child.__owl__.parent).toBe(parent); expect(child.__owl__.parent).toBe(parent);
expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot(); const subId = env.qweb.subTemplates["sub"];
const subFn = QWeb.subTemplates[subId] as any;
expect(subFn.toString()).toMatchSnapshot();
}); });
test("handlers with arguments are properly bound through a t-call", async () => { test("handlers with arguments are properly bound through a t-call", async () => {
@@ -3905,7 +4094,9 @@ describe("t-call", () => {
} }
const parent = new Parent(); const parent = new Parent();
await parent.mount(fixture); await parent.mount(fixture);
expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot(); const subId = env.qweb.subTemplates["sub"];
const subFn = QWeb.subTemplates[subId] as any;
expect(subFn.toString()).toMatchSnapshot();
fixture.querySelector("p")!.click(); fixture.querySelector("p")!.click();
}); });
+40 -15
View File
@@ -92,7 +92,7 @@ describe("props validation", () => {
{ type: String, ok: "1", ko: 1 }, { type: String, ok: "1", ko: 1 },
{ type: Object, ok: {}, ko: "1" }, { type: Object, ok: {}, ko: "1" },
{ type: Date, ok: new Date(), ko: "1" }, { type: Date, ok: new Date(), ko: "1" },
{ type: Function, ok: () => {}, ko: "1" } { type: Function, ok: () => {}, ko: "1" },
]; ];
let props; let props;
@@ -149,7 +149,7 @@ describe("props validation", () => {
{ type: String, ok: "1", ko: 1 }, { type: String, ok: "1", ko: 1 },
{ type: Object, ok: {}, ko: "1" }, { type: Object, ok: {}, ko: "1" },
{ type: Date, ok: new Date(), ko: "1" }, { type: Date, ok: new Date(), ko: "1" },
{ type: Function, ok: () => {}, ko: "1" } { type: Function, ok: () => {}, ko: "1" },
]; ];
let props; let props;
@@ -396,7 +396,7 @@ describe("props validation", () => {
class TestWidget extends Component { class TestWidget extends Component {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
static props = { static props = {
p: { type: Object, shape: { id: Number, url: String } } p: { type: Object, shape: { id: Number, url: String } },
}; };
} }
class Parent extends Component { class Parent extends Component {
@@ -458,9 +458,9 @@ describe("props validation", () => {
type: Object, type: Object,
shape: { shape: {
id: Number, id: Number,
url: [Boolean, { type: Array, element: Number }] url: [Boolean, { type: Array, element: Number }],
} },
} },
}; };
} }
class Parent extends Component { class Parent extends Component {
@@ -510,10 +510,10 @@ describe("props validation", () => {
element: { element: {
type: Object, type: Object,
shape: { shape: {
num: { type: Number, optional: true } num: { type: Number, optional: true },
} },
} },
} },
}; };
} }
let error; let error;
@@ -539,8 +539,8 @@ describe("props validation", () => {
class TestComponent extends Component { class TestComponent extends Component {
static props = { static props = {
size: { size: {
validate: e => ["small", "medium", "large"].includes(e) validate: (e) => ["small", "medium", "large"].includes(e),
} },
}; };
} }
let error; let error;
@@ -561,13 +561,13 @@ describe("props validation", () => {
}); });
test("can validate with a custom validator, and a type", () => { test("can validate with a custom validator, and a type", () => {
const validator = jest.fn(n => 0 <= n && n <= 10); const validator = jest.fn((n) => 0 <= n && n <= 10);
class TestComponent extends Component { class TestComponent extends Component {
static props = { static props = {
n: { n: {
type: Number, type: Number,
validate: validator validate: validator,
} },
}; };
} }
let error; let error;
@@ -786,6 +786,31 @@ describe("props validation", () => {
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div><div>4</div></div>"); expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
}); });
test("mix of optional and mandatory", async () => {
class Child extends Component {
static props = {
optional: { type: String, optional: true },
mandatory: Number,
};
static template = xml` <div><t t-esc="props.mandatory"/></div>`;
}
class App extends Component {
static components = { Child };
static template = xml`<div><Child/></div>`;
}
const w = new App(undefined, {});
let error;
try {
await w.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Missing props 'mandatory' (component 'Child')");
});
}); });
describe("default props", () => { describe("default props", () => {
+128 -5
View File
@@ -28,7 +28,7 @@ afterEach(() => {
function children(w: Component): Component[] { function children(w: Component): Component[] {
const childrenMap = w.__owl__.children; const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]); return Object.keys(childrenMap).map((id) => childrenMap[id]);
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -224,8 +224,8 @@ describe("t-slot directive", () => {
state = useState({ state = useState({
users: [ users: [
{ id: 1, name: "Aaron" }, { id: 1, name: "Aaron" },
{ id: 2, name: "David" } { id: 2, name: "David" },
] ],
}); });
static components = { Link }; static components = { Link };
} }
@@ -268,8 +268,8 @@ describe("t-slot directive", () => {
state = useState({ state = useState({
users: [ users: [
{ id: 1, name: "Aaron" }, { id: 1, name: "Aaron" },
{ id: 2, name: "David" } { id: 2, name: "David" },
] ],
}); });
static components = { Link }; static components = { Link };
} }
@@ -929,4 +929,127 @@ describe("t-slot directive", () => {
expect(fixture.innerHTML).toBe("<div><span>dash</span></div>"); expect(fixture.innerHTML).toBe("<div><span>dash</span></div>");
expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot();
}); });
test("slot and t-esc", async () => {
class Dialog extends Component {
static template = xml`<span><t t-slot="default"/></span>`;
}
class Parent extends Component {
static template = xml`<div><Dialog><t t-esc="'toph'"/></Dialog></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>toph</span></div>");
});
test("slot and (inline) t-esc", async () => {
class Dialog extends Component {
static template = xml`<span><t t-slot="default"/></span>`;
}
class Parent extends Component {
static template = xml`<div><Dialog t-esc="'toph'"/></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>toph</span></div>");
});
test("slot and t-call", async () => {
env.qweb.addTemplate("sokka", "<p>sokka</p>");
class Dialog extends Component {
static template = xml`<span><t t-slot="default"/></span>`;
}
class Parent extends Component {
static template = xml`<div><Dialog><t t-call="sokka"/></Dialog></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span><p>sokka</p></span></div>");
});
test("slot and (inline) t-call", async () => {
env.qweb.addTemplate("sokka", "<p>sokka</p>");
class Dialog extends Component {
static template = xml`<span><t t-slot="default"/></span>`;
}
class Parent extends Component {
static template = xml`<div><Dialog t-call="sokka"/></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span><p>sokka</p></span></div>");
});
test("named slot inside slot", async () => {
class Child extends Component {
static template = xml`
<div>
<t t-slot="brol"/>
<t t-slot="default"/>
</div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child>
<t t-set-slot="brol">
<p>A<t t-esc="value"/></p>
</t>
<Child>
<t t-set-slot="brol">
<p>B<t t-esc="value"/></p>
</t>
</Child>
</Child>
</div>`;
static components = { Child };
value = "blip";
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><p>Ablip</p><div><p>Bblip</p></div></div></div>");
});
test("named slots inside slot, again", async () => {
class Child extends Component {
static template = xml`
<child>
<t t-slot="brol1">default1</t>
<t t-slot="brol2">default2</t>
<t t-slot="default"/>
</child>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child>
<t t-set-slot="brol1">
<p>A<t t-esc="value"/></p>
</t>
<Child>
<t t-set-slot="brol2">
<p>B<t t-esc="value"/></p>
</t>
</Child>
</Child>
</div>`;
static components = { Child };
value = "blip";
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><child><p>Ablip</p>default2<child>default1<p>Bblip</p></child></child></div>"
);
});
}); });
+119 -1
View File
@@ -2,6 +2,7 @@ import { Component, Env } from "../../src/component/component";
import { useState } from "../../src/hooks"; import { useState } from "../../src/hooks";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
import { makeDeferred, makeTestEnv, makeTestFixture, nextTick, nextMicroTick } from "../helpers"; import { makeDeferred, makeTestEnv, makeTestFixture, nextTick, nextMicroTick } from "../helpers";
import { scheduler } from "../../src/component/scheduler";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Setup and helpers // Setup and helpers
@@ -335,7 +336,7 @@ describe("unmounting and remounting", () => {
class Child extends Component { class Child extends Component {
static template = xml`<span t-esc="state.val"/>`; static template = xml`<span t-esc="state.val"/>`;
state = useState({ state = useState({
val: "C1" val: "C1",
}); });
constructor(parent, props) { constructor(parent, props) {
super(parent, props); super(parent, props);
@@ -490,4 +491,121 @@ describe("unmounting and remounting", () => {
expect(steps).toEqual(["1 resolved", "2 resolved"]); expect(steps).toEqual(["1 resolved", "2 resolved"]);
expect(fixture.innerHTML).toBe("<div>Hey</div>"); expect(fixture.innerHTML).toBe("<div>Hey</div>");
}); });
test("mounting a destroyed widget", async () => {
class MyWidget extends Component {
static template = xml`<div>Hey</div>`;
}
const w = new MyWidget();
w.destroy(); // because, why not
let error;
try {
await w.mount(fixture);
} catch (e) {
error = e;
}
expect(scheduler.tasks.length).toBe(0);
expect(error).toBeDefined();
expect(error.message).toBe("Cannot mount a destroyed component");
});
test("destroying a sub-component cleans itself from parent's vnode", async () => {
class C1 extends Component {
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
}
class P extends Component {
static components = { C1 };
static template = xml`<div><div><C1 t-props="state" t-if="state.a"/></div></div>`;
state = {
a: "first",
};
}
const parent = new P();
await parent.mount(fixture);
expect(fixture.textContent).toBe("first");
parent.unmount();
parent.state.a = "";
parent.mount(fixture);
parent.state.a = "fixed";
await parent.render();
expect(fixture.textContent).toBe("fixed");
});
test("destroying a sub-component cleans itself from parent's vnode, part 2", async () => {
class C1 extends Component {
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
}
class P extends Component {
static components = { C1 };
static template = xml`<div><div><C1 t-props="state" t-if="state.a"/>some text</div></div>`;
state = {
a: "first",
};
}
const parent = new P();
await parent.mount(fixture);
expect(fixture.textContent).toBe("firstsome text");
parent.unmount();
parent.state.a = "";
parent.mount(fixture);
parent.state.a = "fixed";
await parent.render();
expect(fixture.textContent).toBe("fixedsome text");
});
test("destroying a sub-component cleans itself from parent's vnode, part 3", async () => {
class C1 extends Component {
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
}
class C2 extends Component {
static template = xml`<C1 a="props.a"/>`;
static components = { C1 };
}
class P extends Component {
static components = { C2 };
static template = xml`<div><div><C2 t-props="state" t-if="state.a"/></div></div>`;
state = {
a: "first",
};
}
const parent = new P();
await parent.mount(fixture);
expect(fixture.textContent).toBe("first");
parent.unmount();
parent.state.a = "";
parent.mount(fixture);
parent.state.a = "fixed";
await parent.render();
expect(fixture.textContent).toBe("fixed");
});
test("destroying a sub-component cleans itself from parent's vnode, part 4", async () => {
class C1 extends Component {
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
}
class C2 extends Component {
static template = xml`<C1 a="props.a"/>`;
static components = { C1 };
}
class P extends Component {
static components = { C2 };
static template = xml`<div><div><C2 t-props="state" t-if="state.a"/>some text</div></div>`;
state = {
a: "first",
};
}
const parent = new P();
await parent.mount(fixture);
expect(fixture.textContent).toBe("firstsome text");
parent.unmount();
parent.state.a = "";
parent.mount(fixture);
parent.state.a = "fixed";
await parent.render();
expect(fixture.textContent).toBe("fixedsome text");
});
}); });
+1 -1
View File
@@ -14,7 +14,7 @@ describe("event bus behaviour", () => {
expect.assertions(1); expect.assertions(1);
const bus = new EventBus(); const bus = new EventBus();
const owner = {}; const owner = {};
bus.on("event", owner, function(this: any) { bus.on("event", owner, function (this: any) {
expect(this).toBe(owner); expect(this).toBe(owner);
}); });
bus.trigger("event"); bus.trigger("event");
+19 -2
View File
@@ -22,7 +22,7 @@ describe("observer", () => {
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
expect(obj2).toEqual({ expect(obj2).toEqual({
a: 2 a: 2,
}); });
}); });
@@ -47,7 +47,7 @@ describe("observer", () => {
expect(observer.rev).toBe(5); expect(observer.rev).toBe(5);
expect(obj).toEqual({ expect(obj).toEqual({
a: null, a: null,
b: undefined b: undefined,
}); });
}); });
@@ -68,6 +68,23 @@ describe("observer", () => {
expect(obj.date).not.toBe(date); expect(obj.date).not.toBe(date);
}); });
test("properly handle promises (i.e.: treat them like primitive values", async () => {
const observer = new Observer();
let resolved = false;
const prom = new Promise((r) => r());
const obj: any = observer.observe({ prom });
expect(obj.prom).toBeInstanceOf(Promise);
obj.prom.then(() => (resolved = true));
expect(observer.revNumber(obj)).toBe(1);
expect(resolved).toBe(false);
await Promise.resolve();
expect(resolved).toBe(true);
expect(observer.revNumber(obj)).toBe(1);
});
test("can change values in array", () => { test("can change values in array", () => {
const observer = new Observer(); const observer = new Observer();
const obj: any = observer.observe({ arr: [1, 2] }); const obj: any = observer.observe({ arr: [1, 2] });
+3 -10
View File
@@ -8,12 +8,6 @@ import "../src/qweb/extensions";
import "../src/component/directive"; import "../src/component/directive";
import { browser } from "../src/browser"; import { browser } from "../src/browser";
// modifies scheduler to make it faster to test components
scheduler.requestAnimationFrame = function(callback: FrameRequestCallback) {
setTimeout(callback, 1);
return 1;
};
// Some static cleanup // Some static cleanup
let nextSlotId; let nextSlotId;
let slots; let slots;
@@ -43,9 +37,8 @@ export function nextMicroTick(): Promise<void> {
} }
export async function nextTick(): Promise<void> { export async function nextTick(): Promise<void> {
return new Promise(function(resolve) { await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
setTimeout(() => scheduler.requestAnimationFrame(() => resolve())); await new Promise((resolve) => setTimeout(resolve));
});
} }
export function makeTestFixture() { export function makeTestFixture() {
@@ -77,7 +70,7 @@ export function makeDeferred(): Deferred {
export function makeTestEnv(): Env { export function makeTestEnv(): Env {
return { return {
qweb: new QWeb(), qweb: new QWeb(),
browser: browser browser: browser,
}; };
} }
+7 -7
View File
@@ -10,7 +10,7 @@ import {
onWillStart, onWillStart,
onWillUpdateProps, onWillUpdateProps,
useSubEnv, useSubEnv,
useExternalListener useExternalListener,
} from "../src/hooks"; } from "../src/hooks";
import { xml } from "../src/tags"; import { xml } from "../src/tags";
@@ -214,7 +214,7 @@ describe("hooks", () => {
"hook:mounted1", "hook:mounted1",
"hook:mounted2", "hook:mounted2",
"hook:willunmount2", "hook:willunmount2",
"hook:willunmount1" "hook:willunmount1",
]); ]);
}); });
@@ -267,7 +267,7 @@ describe("hooks", () => {
class WidgetC extends Component { class WidgetC extends Component {
static template = xml`<div class="outer-div">Hello<WidgetB t-ref="mywidgetb" /></div>`; static template = xml`<div class="outer-div">Hello<WidgetB t-ref="mywidgetb" /></div>`;
static components = { WidgetB }; static components = { WidgetB };
ref = useRef("mywidgetb"); ref = useRef<WidgetB>("mywidgetb");
} }
const widget = new WidgetC(); const widget = new WidgetC();
@@ -560,7 +560,7 @@ describe("hooks", () => {
test("can use onWillStart, onWillUpdateProps", async () => { test("can use onWillStart, onWillUpdateProps", async () => {
const steps: string[] = []; const steps: string[] = [];
async function slow(): Promise<string> { async function slow(): Promise<string> {
return new Promise(resolve => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
resolve("slow"); resolve("slow");
}, 0); }, 0);
@@ -571,7 +571,7 @@ describe("hooks", () => {
steps.push(await slow()); steps.push(await slow());
steps.push("onWillStart"); steps.push("onWillStart");
}); });
onWillUpdateProps(async nextProps => { onWillUpdateProps(async (nextProps) => {
expect(nextProps).toEqual({ value: 2 }); expect(nextProps).toEqual({ value: 2 });
steps.push(await slow()); steps.push(await slow());
steps.push("onWillUpdateProps"); steps.push("onWillUpdateProps");
@@ -581,7 +581,7 @@ describe("hooks", () => {
onWillStart(() => { onWillStart(() => {
steps.push("on2ndStart"); steps.push("on2ndStart");
}); });
onWillUpdateProps(nextProps => { onWillUpdateProps((nextProps) => {
expect(nextProps).toEqual({ value: 2 }); expect(nextProps).toEqual({ value: 2 });
steps.push("on2ndUpdate"); steps.push("on2ndUpdate");
}); });
@@ -623,7 +623,7 @@ describe("hooks", () => {
"onWillStart", "onWillStart",
"on2ndUpdate", "on2ndUpdate",
"slow", "slow",
"onWillUpdateProps" "onWillUpdateProps",
]); ]);
}); });
+4 -4
View File
@@ -486,7 +486,7 @@ describe("Portal: Basic use and DOM placement", () => {
"parent:mounted", "parent:mounted",
"parent:willPatch", "parent:willPatch",
"child:mounted", "child:mounted",
"parent:patched" "parent:patched",
]); ]);
parent.state.val = 2; parent.state.val = 2;
@@ -499,7 +499,7 @@ describe("Portal: Basic use and DOM placement", () => {
"parent:willPatch", "parent:willPatch",
"child:willPatch", "child:willPatch",
"child:patched", "child:patched",
"parent:patched" "parent:patched",
]); ]);
parent.state.hasChild = false; parent.state.hasChild = false;
@@ -515,7 +515,7 @@ describe("Portal: Basic use and DOM placement", () => {
"parent:patched", "parent:patched",
"parent:willPatch", "parent:willPatch",
"child:willUnmount", "child:willUnmount",
"parent:patched" "parent:patched",
]); ]);
}); });
@@ -741,7 +741,7 @@ describe("Portal: Events handling", () => {
steps.push(ev.type as string); steps.push(ev.type as string);
} }
} }
const bodyListener = ev => { const bodyListener = (ev) => {
steps.push(`body: ${ev.type}`); steps.push(`body: ${ev.type}`);
}; };
document.body.addEventListener("click", bodyListener); document.body.addEventListener("click", bodyListener);
+454 -162
View File
@@ -1,5 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`attributes class and t-att-class should combine together 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _1 = utils.toObj(scope['value']);
Object.assign(_1, {'hello':true})
let c3 = [], p3 = {key:3,class:_1};
let vn3 = h('div', p3, c3);
return vn3;
}"
`;
exports[`attributes class and t-attf-class with ternary operation 1`] = ` exports[`attributes class and t-attf-class with ternary operation 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -615,6 +630,177 @@ exports[`foreach iterate, position 1`] = `
}" }"
`; `;
exports[`foreach t-call with body in t-foreach in t-foreach 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let _2 = scope['numbers'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
}
let _length3 = _3.length;
let _origScope5 = scope;
scope = Object.create(scope);
for (let i1 = 0; i1 < _length3; i1++) {
scope.a_first = i1 === 0
scope.a_last = i1 === _length3 - 1
scope.a_index = i1
scope.a = _3[i1]
scope.a_value = _4[i1]
let key1 = i1;
let _6 = scope['letters'];
if (!_6) { throw new Error('QWeb error: Invalid loop expression')}
let _7 = _8 = _6;
if (!(_6 instanceof Array)) {
_7 = Object.keys(_6);
_8 = Object.values(_6);
}
let _length7 = _7.length;
let _origScope9 = scope;
scope = Object.create(scope);
for (let i2 = 0; i2 < _length7; i2++) {
scope.b_first = i2 === 0
scope.b_last = i2 === _length7 - 1
scope.b_index = i2
scope.b = _7[i2]
scope.b_value = _8[i2]
let key2 = i2;
let _origScope13 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
{
let c__0 = [];
utils.getScope(scope, 'c').c = 'x'+'_'+scope['a']+'_'+scope['b'];
scope[utils.zero] = c__0;
}
let k14 = \`__14__\${key1}__\${key2}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: k14}));
}
scope = _origScope13;
}
scope = _origScope9;
let c15 = [], p15 = {key:\`\${key1}_15\`};
let vn15 = h('span', p15, c15);
c1.push(vn15);
if (scope.c != null) {
c15.push({text: scope.c});
}
}
scope = _origScope5;
let c16 = [], p16 = {key:16};
let vn16 = h('span', p16, c16);
c1.push(vn16);
c16.push({text: \`[\`});
let _17 = scope['a'];
if (_17 != null) {
c16.push({text: _17});
}
c16.push({text: \`][\`});
let _18 = scope['b'];
if (_18 != null) {
c16.push({text: _18});
}
c16.push({text: \`][\`});
if (scope.c != null) {
c16.push({text: scope.c});
}
c16.push({text: \`]\`});
return vn1;
}"
`;
exports[`foreach t-call without body in t-foreach in t-foreach 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let _2 = scope['numbers'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
}
let _length3 = _3.length;
let _origScope5 = scope;
scope = Object.create(scope);
for (let i1 = 0; i1 < _length3; i1++) {
scope.a_first = i1 === 0
scope.a_last = i1 === _length3 - 1
scope.a_index = i1
scope.a = _3[i1]
scope.a_value = _4[i1]
let key1 = i1;
let _6 = scope['letters'];
if (!_6) { throw new Error('QWeb error: Invalid loop expression')}
let _7 = _8 = _6;
if (!(_6 instanceof Array)) {
_7 = Object.keys(_6);
_8 = Object.values(_6);
}
let _length7 = _7.length;
let _origScope9 = scope;
scope = Object.create(scope);
for (let i2 = 0; i2 < _length7; i2++) {
scope.b_first = i2 === 0
scope.b_last = i2 === _length7 - 1
scope.b_index = i2
scope.b = _7[i2]
scope.b_value = _8[i2]
let key2 = i2;
let _origScope12 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
let k13 = \`__13__\${key1}__\${key2}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: k13}));
scope = _origScope12;
}
scope = _origScope9;
let c14 = [], p14 = {key:\`\${key1}_14\`};
let vn14 = h('span', p14, c14);
c1.push(vn14);
let _15 = scope['c'];
if (_15 != null) {
c14.push({text: _15});
}
}
scope = _origScope5;
let c16 = [], p16 = {key:16};
let vn16 = h('span', p16, c16);
c1.push(vn16);
c16.push({text: \`[\`});
let _17 = scope['a'];
if (_17 != null) {
c16.push({text: _17});
}
c16.push({text: \`][\`});
let _18 = scope['b'];
if (_18 != null) {
c16.push({text: _18});
}
c16.push({text: \`][\`});
let _19 = scope['c'];
if (_19 != null) {
c16.push({text: _19});
}
c16.push({text: \`]\`});
return vn1;
}"
`;
exports[`foreach t-foreach in t-forach 1`] = ` exports[`foreach t-foreach in t-forach 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -734,7 +920,11 @@ exports[`loading templates can load a few templates from a xml string 1`] = `
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('ul', p1, c1); let vn1 = h('ul', p1, c1);
this.subTemplates['items'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'})); let _origScope4 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'}));
scope = _origScope4;
return vn1; return vn1;
}" }"
`; `;
@@ -772,39 +962,51 @@ exports[`misc global 1`] = `
if (_7 != null) { if (_7 != null) {
c6.push({text: _7}); c6.push({text: _7});
} }
let _origScope10 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope10 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
let _origScope13 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope13 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
utils.getScope(scope, 'foo').foo = 'aaa'; utils.getScope(scope, 'foo').foo = 'aaa';
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
let k14 = \`__14__\${key1}__\`; let k14 = \`__14__\${key1}__\`;
this.subTemplates['_callee-uses-foo'].call(this, scope, Object.assign({}, extra, {parentNode: c__0, parent: utils.getComponent(context), key: k14})); this.constructor.subTemplates['2'].call(this, scope, Object.assign({}, extra, {parentNode: c__0, parent: utils.getComponent(context), key: k14}));
scope = _origScope13;
} }
let k15 = \`__15__\${key1}__\`; scope = _origScope13;
this.subTemplates['_callee-uses-foo'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c__0, parent: utils.getComponent(context), key: k15})); let _origScope15 = scope;
utils.getScope(scope, 'foo').foo = 'bbb'; scope = Object.create(scope);
scope.__access_mode__ = 'ro';
let k16 = \`__16__\${key1}__\`; let k16 = \`__16__\${key1}__\`;
this.subTemplates['_callee-uses-foo'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c__0, parent: utils.getComponent(context), key: k16})); this.constructor.subTemplates['2'].call(this, scope, Object.assign({}, extra, {parentNode: c__0, parent: utils.getComponent(context), key: k16}));
scope = _origScope15;
utils.getScope(scope, 'foo').foo = 'bbb';
let _origScope17 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
let k18 = \`__18__\${key1}__\`;
this.constructor.subTemplates['2'].call(this, scope, Object.assign({}, extra, {parentNode: c__0, parent: utils.getComponent(context), key: k18}));
scope = _origScope17;
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
let k17 = \`__17__\${key1}__\`; let k19 = \`__19__\${key1}__\`;
this.subTemplates['_callee-asc'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: k17})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: k19}));
scope = _origScope10;
} }
scope = _origScope10;
} }
scope = _origScope5; scope = _origScope5;
this.subTemplates['_callee-asc-toto'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__20__'})); let _origScope22 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['3'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__23__'}));
scope = _origScope22;
return vn1; return vn1;
}" }"
`; `;
@@ -857,7 +1059,7 @@ exports[`properly support svg add proper namespace to svg 1`] = `
}" }"
`; `;
exports[`special cases for some boolean html attributes/properties input type= checkbox, with t-att-checked 1`] = ` exports[`special cases for some specific html attributes/properties input type= checkbox, with t-att-checked 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"test\\" // Template name: \\"test\\"
@@ -871,7 +1073,7 @@ exports[`special cases for some boolean html attributes/properties input type= c
}" }"
`; `;
exports[`special cases for some boolean html attributes/properties various boolean html attributes 1`] = ` exports[`special cases for some specific html attributes/properties various boolean html attributes 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"test\\" // Template name: \\"test\\"
@@ -1093,25 +1295,15 @@ exports[`t-call (template calling basic caller 1`] = `
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
this.subTemplates['_basic-callee'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__3__'})); let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'}));
scope = _origScope3;
return vn1; return vn1;
}" }"
`; `;
exports[`t-call (template calling basic caller 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"_basic-callee\\"
let h = this.h;
let c1 = extra.parentNode;
let key0 = extra.key || \\"\\";
let c2 = [], p2 = {key:\`\${key0}_2\`};
let vn2 = h('span', p2, c2);
c1.push(vn2);
c2.push({text: \`ok\`});
}"
`;
exports[`t-call (template calling basic caller, no parent node 1`] = ` exports[`t-call (template calling basic caller, no parent node 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -1120,27 +1312,17 @@ exports[`t-call (template calling basic caller, no parent node 1`] = `
let scope = Object.create(context); let scope = Object.create(context);
let result; let result;
let h = this.h; let h = this.h;
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
result = [] result = []
this.subTemplates['_basic-callee'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: result, parent: utils.getComponent(context), key: '__3__'})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: result, parent: utils.getComponent(context), key: '__4__'}));
result = result[0] result = result[0]
scope = _origScope3;
return result; return result;
}" }"
`; `;
exports[`t-call (template calling basic caller, no parent node 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"_basic-callee\\"
let h = this.h;
let c1 = extra.parentNode;
let key0 = extra.key || \\"\\";
let c2 = [], p2 = {key:\`\${key0}_2\`};
let vn2 = h('div', p2, c2);
c1.push(vn2);
c2.push({text: \`ok\`});
}"
`;
exports[`t-call (template calling call with several sub nodes on same line 1`] = ` exports[`t-call (template calling call with several sub nodes on same line 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -1150,10 +1332,10 @@ exports[`t-call (template calling call with several sub nodes on same line 1`] =
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
let c4 = [], p4 = {key:4}; let c4 = [], p4 = {key:4};
@@ -1167,28 +1349,14 @@ exports[`t-call (template calling call with several sub nodes on same line 1`] =
c5.push({text: \`yay\`}); c5.push({text: \`yay\`});
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
this.subTemplates['SubTemplate'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__6__'})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__6__'}));
scope = _origScope3;
} }
scope = _origScope3;
return vn1; return vn1;
}" }"
`; `;
exports[`t-call (template calling call with several sub nodes on same line 2`] = ` exports[`t-call (template calling call with several sub nodes on same line 2`] = `"1"`;
"function anonymous(context, extra
) {
// Template name: \\"SubTemplate\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c1 = extra.parentNode;
let key0 = extra.key || \\"\\";
let c2 = [], p2 = {key:\`\${key0}_2\`};
let vn2 = h('div', p2, c2);
c1.push(vn2);
c2.push(...scope[utils.zero]);
}"
`;
exports[`t-call (template calling cascading t-call t-raw='0' 1`] = ` exports[`t-call (template calling cascading t-call t-raw='0' 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
@@ -1199,10 +1367,10 @@ exports[`t-call (template calling cascading t-call t-raw='0' 1`] = `
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
let _origScope12 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope12 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
let c13 = [], p13 = {key:13}; let c13 = [], p13 = {key:13};
@@ -1216,9 +1384,9 @@ exports[`t-call (template calling cascading t-call t-raw='0' 1`] = `
c14.push({text: \`yay\`}); c14.push({text: \`yay\`});
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
this.subTemplates['SubTemplate'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__15__'})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__15__'}));
scope = _origScope12;
} }
scope = _origScope12;
return vn1; return vn1;
}" }"
`; `;
@@ -1233,7 +1401,11 @@ exports[`t-call (template calling inherit context 1`] = `
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
scope.foo = 1; scope.foo = 1;
this.subTemplates['_callee-uses-foo'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__2__'})); let _origScope2 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__3__'}));
scope = _origScope2;
return vn1; return vn1;
}" }"
`; `;
@@ -1252,7 +1424,11 @@ exports[`t-call (template calling recursive template, part 1 1`] = `
c1.push(vn2); c1.push(vn2);
c2.push({text: \`hey\`}); c2.push({text: \`hey\`});
if (false) { if (false) {
this.subTemplates['recursive'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__6__'})); let _origScope7 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__8__'}));
scope = _origScope7;
} }
return vn1; return vn1;
}" }"
@@ -1275,8 +1451,12 @@ exports[`t-call (template calling recursive template, part 1 2`] = `
c3.push(vn4); c3.push(vn4);
c4.push({text: \`hey\`}); c4.push({text: \`hey\`});
if (false) { if (false) {
let k5 = \`__5__\${key0}__\`; let _origScope5 = scope;
this.subTemplates['recursive'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c3, parent: utils.getComponent(context), key: k5})); scope = Object.create(scope);
scope.__access_mode__ = 'ro';
let k6 = \`__6__\${key0}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: utils.getComponent(context), key: k6}));
scope = _origScope5;
} }
}" }"
`; `;
@@ -1290,18 +1470,18 @@ exports[`t-call (template calling recursive template, part 2 1`] = `
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
let _origScope11 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope11 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
utils.getScope(scope, 'node').node = scope['root']; utils.getScope(scope, 'node').node = scope['root'];
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
this.subTemplates['nodeTemplate'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__12__'})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__12__'}));
scope = _origScope11;
} }
scope = _origScope11;
return vn1; return vn1;
}" }"
`; `;
@@ -1342,19 +1522,19 @@ exports[`t-call (template calling recursive template, part 2 2`] = `
scope.subtree = _6[i1] scope.subtree = _6[i1]
scope.subtree_value = _7[i1] scope.subtree_value = _7[i1]
let key1 = i1; let key1 = i1;
let _origScope9 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope9 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
utils.getScope(scope, 'node').node = scope['subtree']; utils.getScope(scope, 'node').node = scope['subtree'];
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
let k10 = \`__10__\${key0}__\${key1}__\`; let k10 = \`__10__\${key0}__\${key1}__\`;
this.subTemplates['nodeTemplate'].call(this, scope, Object.assign({}, extra, {parentNode: c2, parent: utils.getComponent(context), key: k10})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c2, parent: utils.getComponent(context), key: k10}));
scope = _origScope9;
} }
scope = _origScope9;
} }
scope = _origScope8; scope = _origScope8;
}" }"
@@ -1369,18 +1549,18 @@ exports[`t-call (template calling recursive template, part 3 1`] = `
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
let _origScope11 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope11 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
utils.getScope(scope, 'node').node = scope['root']; utils.getScope(scope, 'node').node = scope['root'];
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
this.subTemplates['nodeTemplate'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__12__'})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__12__'}));
scope = _origScope11;
} }
scope = _origScope11;
return vn1; return vn1;
}" }"
`; `;
@@ -1421,19 +1601,19 @@ exports[`t-call (template calling recursive template, part 3 2`] = `
scope.subtree = _6[i1] scope.subtree = _6[i1]
scope.subtree_value = _7[i1] scope.subtree_value = _7[i1]
let key1 = i1; let key1 = i1;
let _origScope9 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope9 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
utils.getScope(scope, 'node').node = scope['subtree']; utils.getScope(scope, 'node').node = scope['subtree'];
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
let k10 = \`__10__\${key0}__\${key1}__\`; let k10 = \`__10__\${key0}__\${key1}__\`;
this.subTemplates['nodeTemplate'].call(this, scope, Object.assign({}, extra, {parentNode: c2, parent: utils.getComponent(context), key: k10})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c2, parent: utils.getComponent(context), key: k10}));
scope = _origScope9;
} }
scope = _origScope9;
} }
scope = _origScope8; scope = _origScope8;
}" }"
@@ -1448,19 +1628,19 @@ exports[`t-call (template calling recursive template, part 4: with t-set recursi
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
let _origScope11 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope11 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
utils.getScope(scope, 'recursive_idx').recursive_idx = 1; utils.getScope(scope, 'recursive_idx').recursive_idx = 1;
utils.getScope(scope, 'node').node = scope['root']; utils.getScope(scope, 'node').node = scope['root'];
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
this.subTemplates['nodeTemplate'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__12__'})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__12__'}));
scope = _origScope11;
} }
scope = _origScope11;
return vn1; return vn1;
}" }"
`; `;
@@ -1506,19 +1686,19 @@ exports[`t-call (template calling recursive template, part 4: with t-set recursi
scope.subtree = _6[i1] scope.subtree = _6[i1]
scope.subtree_value = _7[i1] scope.subtree_value = _7[i1]
let key1 = i1; let key1 = i1;
let _origScope9 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope9 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
utils.getScope(scope, 'node').node = scope['subtree']; utils.getScope(scope, 'node').node = scope['subtree'];
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
let k10 = \`__10__\${key0}__\${key1}__\`; let k10 = \`__10__\${key0}__\${key1}__\`;
this.subTemplates['nodeTemplate'].call(this, scope, Object.assign({}, extra, {parentNode: c2, parent: utils.getComponent(context), key: k10})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c2, parent: utils.getComponent(context), key: k10}));
scope = _origScope9;
} }
scope = _origScope9;
} }
scope = _origScope8; scope = _origScope8;
}" }"
@@ -1533,18 +1713,18 @@ exports[`t-call (template calling scoped parameters 1`] = `
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
let _origScope2 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope2 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
utils.getScope(scope, 'foo').foo = 42; utils.getScope(scope, 'foo').foo = 42;
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
this.subTemplates['_basic-callee'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__3__'})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__3__'}));
scope = _origScope2;
} }
scope = _origScope2;
if (scope.foo != null) { if (scope.foo != null) {
c1.push({text: scope.foo}); c1.push({text: scope.foo});
} }
@@ -1552,6 +1732,24 @@ exports[`t-call (template calling scoped parameters 1`] = `
}" }"
`; `;
exports[`t-call (template calling t-call allowed on a non t node 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"caller\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'}));
scope = _origScope3;
return vn1;
}"
`;
exports[`t-call (template calling t-call with t-if 1`] = ` exports[`t-call (template calling t-call with t-if 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -1562,23 +1760,40 @@ exports[`t-call (template calling t-call with t-if 1`] = `
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
if (scope['flag']) { if (scope['flag']) {
this.subTemplates['sub'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__3__'})); let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'}));
scope = _origScope3;
} }
return vn1; return vn1;
}" }"
`; `;
exports[`t-call (template calling t-call with t-if 2`] = ` exports[`t-call (template calling t-call with t-set inside and body text content 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"sub\\" // Template name: \\"main\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h; let h = this.h;
let c1 = extra.parentNode; let c1 = [], p1 = {key:1};
let key0 = extra.key || \\"\\"; let vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:\`\${key0}_2\`}; let _origScope4 = scope;
let vn2 = h('span', p2, c2); scope = Object.create(scope);
c1.push(vn2); scope.__access_mode__ = 'ro';
c2.push({text: \`ok\`}); {
{
let c__0 = [];
let c5 = new utils.VDomArray();
c5.push({text: \`yip yip\`});
scope.val = c5
scope[utils.zero] = c__0;
}
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__6__'}));
}
scope = _origScope4;
return vn1;
}" }"
`; `;
@@ -1609,19 +1824,19 @@ exports[`t-call (template calling t-call with t-set inside and outside 1`] = `
scope.v_value = _4[i1] scope.v_value = _4[i1]
let key1 = i1; let key1 = i1;
utils.getScope(scope, 'val').val = scope['v'].val; utils.getScope(scope, 'val').val = scope['v'].val;
let _origScope8 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope8 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
utils.getScope(scope, 'val3').val3 = scope.val*3; utils.getScope(scope, 'val3').val3 = scope.val*3;
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
let k9 = \`__9__\${key1}__\`; let k9 = \`__9__\${key1}__\`;
this.subTemplates['sub'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: k9})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: k9}));
scope = _origScope8;
} }
scope = _origScope8;
} }
scope = _origScope5; scope = _origScope5;
return vn1; return vn1;
@@ -1638,7 +1853,11 @@ exports[`t-call (template calling t-call with t-set inside and outside. 2 1`] =
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('p', p1, c1); let vn1 = h('p', p1, c1);
scope.w = 'fromwrapper'; scope.w = 'fromwrapper';
this.subTemplates['main'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__11__'})); let _origScope11 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__12__'}));
scope = _origScope11;
return vn1; return vn1;
}" }"
`; `;
@@ -1654,21 +1873,25 @@ exports[`t-call (template calling t-call, conditional and t-set in t-call body 1
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
scope.v1 = 'elif'; scope.v1 = 'elif';
if (scope.v1==='if') { if (scope.v1==='if') {
this.subTemplates['callee1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__3__'})); let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'}));
scope = _origScope3;
} }
else if (scope.v1==='elif') { else if (scope.v1==='elif') {
let _origScope7 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope6 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
utils.getScope(scope, 'v').v = 'success'; utils.getScope(scope, 'v').v = 'success';
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
this.subTemplates['callee2'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__7__'})); this.constructor.subTemplates['2'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__8__'}));
scope = _origScope6;
} }
scope = _origScope7;
} }
return vn1; return vn1;
}" }"
@@ -1683,7 +1906,47 @@ exports[`t-call (template calling t-call, global templates 1`] = `
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
this.subTemplates['john'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__3__'})); let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'}));
scope = _origScope3;
return vn1;
}"
`;
exports[`t-call (template calling two different QWeb instances, and shared templates 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"main\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'}));
scope = _origScope3;
return vn1;
}"
`;
exports[`t-call (template calling two different QWeb instances, and shared templates 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"main\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'}));
scope = _origScope3;
return vn1; return vn1;
}" }"
`; `;
@@ -1696,20 +1959,20 @@ exports[`t-call (template calling with unused body 1`] = `
let scope = Object.create(context); let scope = Object.create(context);
let result; let result;
let h = this.h; let h = this.h;
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
c__0.push({text: \`WHEEE\`}); c__0.push({text: \`WHEEE\`});
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
result = [] result = []
this.subTemplates['_basic-callee'].call(this, scope, Object.assign({}, extra, {parentNode: result, parent: utils.getComponent(context), key: '__4__'})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: result, parent: utils.getComponent(context), key: '__4__'}));
result = result[0] result = result[0]
scope = _origScope3;
} }
scope = _origScope3;
return result; return result;
}" }"
`; `;
@@ -1722,20 +1985,20 @@ exports[`t-call (template calling with unused setbody 1`] = `
let scope = Object.create(context); let scope = Object.create(context);
let result; let result;
let h = this.h; let h = this.h;
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
utils.getScope(scope, 'qux').qux = 3; utils.getScope(scope, 'qux').qux = 3;
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
result = [] result = []
this.subTemplates['_basic-callee'].call(this, scope, Object.assign({}, extra, {parentNode: result, parent: utils.getComponent(context), key: '__4__'})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: result, parent: utils.getComponent(context), key: '__4__'}));
result = result[0] result = result[0]
scope = _origScope3;
} }
scope = _origScope3;
return result; return result;
}" }"
`; `;
@@ -1748,20 +2011,20 @@ exports[`t-call (template calling with used body 1`] = `
let scope = Object.create(context); let scope = Object.create(context);
let result; let result;
let h = this.h; let h = this.h;
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
c__0.push({text: \`ok\`}); c__0.push({text: \`ok\`});
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
result = [] result = []
this.subTemplates['_callee-printsbody'].call(this, scope, Object.assign({}, extra, {parentNode: result, parent: utils.getComponent(context), key: '__4__'})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: result, parent: utils.getComponent(context), key: '__4__'}));
result = result[0] result = result[0]
scope = _origScope3;
} }
scope = _origScope3;
return result; return result;
}" }"
`; `;
@@ -1775,18 +2038,18 @@ exports[`t-call (template calling with used set body 1`] = `
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1); let vn1 = h('span', p1, c1);
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
utils.getScope(scope, 'foo').foo = 'ok'; utils.getScope(scope, 'foo').foo = 'ok';
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
this.subTemplates['_callee-uses-foo'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'}));
scope = _origScope3;
} }
scope = _origScope3;
return vn1; return vn1;
}" }"
`; `;
@@ -1922,6 +2185,27 @@ exports[`t-esc literal 1`] = `
}" }"
`; `;
exports[`t-esc t-esc inside t-call, with t-set outside 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"main\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let c2 = new utils.VDomArray();
c2.push({text: \`Hi\`});
scope.v = c2
let _origScope5 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__6__'}));
scope = _origScope5;
return vn1;
}"
`;
exports[`t-esc t-esc is escaped 1`] = ` exports[`t-esc t-esc is escaped 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -1974,10 +2258,10 @@ exports[`t-esc t-esc=0 is escaped 1`] = `
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let _origScope3 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
let c4 = [], p4 = {key:4}; let c4 = [], p4 = {key:4};
@@ -1986,9 +2270,9 @@ exports[`t-esc t-esc=0 is escaped 1`] = `
c4.push({text: \`escaped\`}); c4.push({text: \`escaped\`});
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
this.subTemplates['test'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'}));
scope = _origScope3;
} }
scope = _origScope3;
return vn1; return vn1;
}" }"
`; `;
@@ -2782,7 +3066,11 @@ exports[`t-on t-on with t-call 1`] = `
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
this.subTemplates['sub'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'})); let _origScope4 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'}));
scope = _origScope4;
return vn1; return vn1;
}" }"
`; `;
@@ -2796,7 +3084,11 @@ exports[`t-on t-on, with arguments and t-call 1`] = `
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
this.subTemplates['sub'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'})); let _origScope4 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'}));
scope = _origScope4;
return vn1; return vn1;
}" }"
`; `;
+183 -55
View File
@@ -195,7 +195,7 @@ describe("t-esc", () => {
v2: undefined, v2: undefined,
v3: null, v3: null,
v4: 0, v4: 0,
v5: "" v5: "",
}; };
expect(renderToString(qweb, "test", vals)).toBe( expect(renderToString(qweb, "test", vals)).toBe(
"<div><p>false</p><p></p><p></p><p>0</p><p></p></div>" "<div><p>false</p><p></p><p></p><p>0</p><p></p></div>"
@@ -207,6 +207,13 @@ describe("t-esc", () => {
const result = renderToString(qweb, "test", { state: { list: [1, 2] } }); const result = renderToString(qweb, "test", { state: { list: [1, 2] } });
expect(result).toBe("<span>1,2</span>"); expect(result).toBe("<span>1,2</span>");
}); });
test("t-esc inside t-call, with t-set outside", () => {
qweb.addTemplate("main", `<div><t t-set="v">Hi</t><t t-call="sub"/></div>`);
qweb.addTemplate("sub", `<span t-esc="v"/>`);
const result = renderToString(qweb, "main");
expect(result).toBe("<div><span>Hi</span></div>");
});
}); });
describe("t-raw", () => { describe("t-raw", () => {
@@ -486,7 +493,7 @@ describe("t-if", () => {
cond3: false, cond3: false,
cond4: false, cond4: false,
m: 5, m: 5,
n: 2 n: 2,
}; };
expect(normalize(renderToString(qweb, "test", context))).toBe("<div>andormgtnlt</div>"); expect(normalize(renderToString(qweb, "test", context))).toBe("<div>andormgtnlt</div>");
}); });
@@ -625,7 +632,7 @@ describe("attributes", () => {
test("object", () => { test("object", () => {
qweb.addTemplate("test", `<div t-att="value"/>`); qweb.addTemplate("test", `<div t-att="value"/>`);
const result = renderToString(qweb, "test", { const result = renderToString(qweb, "test", {
value: { a: 1, b: 2, c: 3 } value: { a: 1, b: 2, c: 3 },
}); });
expect(result).toBe(`<div a="1" b="2" c="3"></div>`); expect(result).toBe(`<div a="1" b="2" c="3"></div>`);
}); });
@@ -684,7 +691,7 @@ describe("attributes", () => {
const result = renderToString(qweb, "test", { const result = renderToString(qweb, "test", {
value1: 0, value1: 0,
value2: 1, value2: 1,
value3: 2 value3: 2,
}); });
expect(result).toBe(`<div foo="a 0 is 1 of 2 ]"></div>`); expect(result).toBe(`<div foo="a 0 is 1 of 2 ]"></div>`);
}); });
@@ -703,7 +710,7 @@ describe("attributes", () => {
const result = renderToString(qweb, "test", { const result = renderToString(qweb, "test", {
bar: 0, bar: 0,
baz: 1, baz: 1,
qux: { qux: "<>" } qux: { qux: "<>" },
}); });
const expected = '<div foo="<foo" bar="0" baz="<1>" qux="<>"></div>'; const expected = '<div foo="<foo" bar="0" baz="<1>" qux="<>"></div>';
expect(result).toBe(expected); expect(result).toBe(expected);
@@ -715,6 +722,12 @@ describe("attributes", () => {
expect(result).toBe(`<div class="hello world"></div>`); expect(result).toBe(`<div class="hello world"></div>`);
}); });
test("class and t-att-class should combine together", () => {
qweb.addTemplate("test", `<div t-att-class="value" class="hello" />`);
const result = renderToString(qweb, "test", { value: "world" });
expect(result).toBe(`<div class="world hello"></div>`);
});
test("class and t-attf-class with ternary operation", () => { test("class and t-attf-class with ternary operation", () => {
qweb.addTemplate("test", `<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`); qweb.addTemplate("test", `<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`);
const result = renderToString(qweb, "test", { value: true }); const result = renderToString(qweb, "test", { value: true });
@@ -734,7 +747,7 @@ describe("t-call (template calling", () => {
qweb.addTemplate("caller", '<div><t t-call="_basic-callee"/></div>'); qweb.addTemplate("caller", '<div><t t-call="_basic-callee"/></div>');
const expected = "<div><span>ok</span></div>"; const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller")).toBe(expected); expect(renderToString(qweb, "caller")).toBe(expected);
expect(qweb.subTemplates["_basic-callee"].toString()).toMatchSnapshot(); expect(qweb.subTemplates["_basic-callee"]).toBeTruthy();
}); });
test("basic caller, no parent node", () => { test("basic caller, no parent node", () => {
@@ -742,7 +755,7 @@ describe("t-call (template calling", () => {
qweb.addTemplate("caller", '<t t-call="_basic-callee"/>'); qweb.addTemplate("caller", '<t t-call="_basic-callee"/>');
const expected = "<div>ok</div>"; const expected = "<div>ok</div>";
expect(renderToString(qweb, "caller")).toBe(expected); expect(renderToString(qweb, "caller")).toBe(expected);
expect(qweb.subTemplates["_basic-callee"].toString()).toMatchSnapshot(); expect(qweb.subTemplates["_basic-callee"]).toBeTruthy();
}); });
test("t-call with t-if", () => { test("t-call with t-if", () => {
@@ -750,13 +763,14 @@ describe("t-call (template calling", () => {
qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>'); qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>');
const expected = "<div><span>ok</span></div>"; const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller", { flag: true })).toBe(expected); expect(renderToString(qweb, "caller", { flag: true })).toBe(expected);
expect(qweb.subTemplates["sub"].toString()).toMatchSnapshot(); expect(qweb.subTemplates["sub"]).toBeTruthy();
}); });
test("t-call not allowed on a non t node", () => { test("t-call allowed on a non t node", () => {
qweb.addTemplate("_basic-callee", "<t>ok</t>"); qweb.addTemplate("_basic-callee", "<span>ok</span>");
qweb.addTemplate("caller", '<div t-call="_basic-callee"/>'); qweb.addTemplate("caller", '<div t-call="_basic-callee"/>');
expect(() => renderToString(qweb, "caller")).toThrow("Invalid tag"); const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller")).toBe(expected);
}); });
test("with unused body", () => { test("with unused body", () => {
@@ -885,7 +899,8 @@ describe("t-call (template calling", () => {
`); `);
const expected = "<div><span>hey</span></div>"; const expected = "<div><span>hey</span></div>";
expect(renderToString(qweb, "recursive")).toBe(expected); expect(renderToString(qweb, "recursive")).toBe(expected);
const recursiveFn = Object.values(qweb.subTemplates)[0] as any; const subId = qweb.subTemplates["recursive"];
const recursiveFn = QWeb.subTemplates[subId] as any;
expect(recursiveFn.toString()).toMatchSnapshot(); expect(recursiveFn.toString()).toMatchSnapshot();
}); });
@@ -913,7 +928,8 @@ describe("t-call (template calling", () => {
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe( expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe(
expected expected
); );
const recursiveFn = Object.values(qweb.subTemplates)[0] as any; const subId = qweb.subTemplates["nodeTemplate"];
const recursiveFn = QWeb.subTemplates[subId] as any;
expect(recursiveFn.toString()).toMatchSnapshot(); expect(recursiveFn.toString()).toMatchSnapshot();
}); });
@@ -940,7 +956,8 @@ describe("t-call (template calling", () => {
const expected = const expected =
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>"; "<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 }, { fiber: {} })).toBe(expected); expect(renderToString(qweb, "Parent", { root }, { fiber: {} })).toBe(expected);
const recursiveFn = Object.values(qweb.subTemplates)[0] as any; const subId = qweb.subTemplates["nodeTemplate"];
const recursiveFn = QWeb.subTemplates[subId] as any;
expect(recursiveFn.toString()).toMatchSnapshot(); expect(recursiveFn.toString()).toMatchSnapshot();
}); });
@@ -967,12 +984,13 @@ describe("t-call (template calling", () => {
`); `);
const root = { const root = {
val: "a", val: "a",
children: [{ val: "b", children: [{ val: "c", children: [{ val: "d" }] }] }] children: [{ val: "b", children: [{ val: "c", children: [{ val: "d" }] }] }],
}; };
const expected = const expected =
"<div><div><p>a 2</p><div><p>b 3</p><div><p>c 4</p><div><p>d 5</p></div></div></div></div></div>"; "<div><div><p>a 2</p><div><p>b 3</p><div><p>c 4</p><div><p>d 5</p></div></div></div></div></div>";
expect(renderToString(qweb, "Parent", { root })).toBe(expected); expect(renderToString(qweb, "Parent", { root })).toBe(expected);
const recursiveFn = Object.values(qweb.subTemplates)[0] as any; const subId = qweb.subTemplates["nodeTemplate"];
const recursiveFn = QWeb.subTemplates[subId] as any;
expect(recursiveFn.toString()).toMatchSnapshot(); expect(recursiveFn.toString()).toMatchSnapshot();
}); });
@@ -1044,6 +1062,32 @@ describe("t-call (template calling", () => {
const context = { list: [{ val: 1 }, { val: 2 }, { val: 3 }] }; const context = { list: [{ val: 1 }, { val: 2 }, { val: 3 }] };
expect(trim(renderToString(qweb, "wrapper", context))).toBe(expected); expect(trim(renderToString(qweb, "wrapper", context))).toBe(expected);
}); });
test("two different QWeb instances, and shared templates", () => {
QWeb.registerTemplate("sub", `<span>ok</span>`);
QWeb.registerTemplate("main", `<div><t t-call="sub"/></div>`);
const qweb1 = new QWeb();
const qweb2 = new QWeb();
expect(renderToString(qweb1, "main")).toBe("<div><span>ok</span></div>");
expect(renderToString(qweb2, "main")).toBe("<div><span>ok</span></div>");
});
test("t-call with t-set inside and body text content", () => {
qweb.addTemplate("sub", `<p><t t-esc="val"/></p>`);
qweb.addTemplate(
"main",
`
<div>
<t t-call="sub">
<t t-set="val">yip yip</t>
</t>
</div>`
);
const expected = "<div><p>yip yip</p></div>";
expect(renderToString(qweb, "main")).toBe(expected);
});
}); });
describe("foreach", () => { describe("foreach", () => {
@@ -1134,6 +1178,63 @@ describe("foreach", () => {
); );
}); });
test("t-call without body in t-foreach in t-foreach", () => {
qweb.addTemplate(
"test_called",
`<t>
<t t-set="c" t-value="'x' + '_' + a + '_'+ b" />
[<t t-esc="a" />]
[<t t-esc="b" />]
[<t t-esc="c" />]
</t>`
);
qweb.addTemplate(
"test",
`<div>
<t t-foreach="numbers" t-as="a">
<t t-foreach="letters" t-as="b">
<t t-call="test_called" />
</t>
<span t-esc="c"/>
</t>
<span>[<t t-esc="a" />][<t t-esc="b" />][<t t-esc="c" />]</span>
</div>`
);
const context = { numbers: [1, 2, 3], letters: ["a", "b"] };
expect(renderToString(qweb, "test", context)).toBe(
"<div> [1] [a] [x_1_a] [1] [b] [x_1_b] <span></span> [2] [a] [x_2_a] [2] [b] [x_2_b] <span></span> [3] [a] [x_3_a] [3] [b] [x_3_b] <span></span><span>[][][]</span></div>"
);
});
test("t-call with body in t-foreach in t-foreach", () => {
qweb.addTemplate(
"test_called",
`<t>
[<t t-esc="a" />]
[<t t-esc="b" />]
[<t t-esc="c" />]
</t>`
);
qweb.addTemplate(
"test",
`<div>
<t t-foreach="numbers" t-as="a">
<t t-foreach="letters" t-as="b">
<t t-call="test_called" >
<t t-set="c" t-value="'x' + '_' + a + '_'+ b" />
</t>
</t>
<span t-esc="c"/>
</t>
<span>[<t t-esc="a" />][<t t-esc="b" />][<t t-esc="c" />]</span>
</div>`
);
const context = { numbers: [1, 2, 3], letters: ["a", "b"] };
expect(renderToString(qweb, "test", context)).toBe(
"<div> [1] [a] [x_1_a] [1] [b] [x_1_b] <span></span> [2] [a] [x_2_a] [2] [b] [x_2_b] <span></span> [3] [a] [x_3_a] [3] [b] [x_3_b] <span></span><span>[][][]</span></div>"
);
});
test("throws error if invalid loop expression", () => { test("throws error if invalid loop expression", () => {
qweb.addTemplate( qweb.addTemplate(
"test", "test",
@@ -1229,7 +1330,7 @@ describe("t-on", () => {
{ {
add() { add() {
a = 3; a = 3;
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1252,7 +1353,7 @@ describe("t-on", () => {
}, },
handleDblClick() { handleDblClick() {
steps.push("dblclick"); steps.push("dblclick");
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1272,7 +1373,7 @@ describe("t-on", () => {
{ {
add(n) { add(n) {
a = a + n; a = a + n;
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1289,7 +1390,7 @@ describe("t-on", () => {
{ {
add({ val }) { add({ val }) {
a = a + val; a = a + val;
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1306,7 +1407,7 @@ describe("t-on", () => {
{ {
doSomething(arg) { doSomething(arg) {
expect(arg).toEqual({}); expect(arg).toEqual({});
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1322,7 +1423,7 @@ describe("t-on", () => {
{ {
doSomething(arg) { doSomething(arg) {
expect(arg).toEqual({}); expect(arg).toEqual({});
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1344,7 +1445,7 @@ describe("t-on", () => {
{ {
activate(action) { activate(action) {
expect(action).toBe("someval"); expect(action).toBe("someval");
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1357,7 +1458,7 @@ describe("t-on", () => {
let owner = { let owner = {
add() { add() {
expect(this).toBe(owner); expect(this).toBe(owner);
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
(<HTMLElement>node).click(); (<HTMLElement>node).click();
@@ -1367,8 +1468,8 @@ describe("t-on", () => {
qweb.addTemplate("test", `<button t-on-click="state.counter++">Click</button>`); qweb.addTemplate("test", `<button t-on-click="state.counter++">Click</button>`);
let owner = { let owner = {
state: { state: {
counter: 0 counter: 0,
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.counter).toBe(0); expect(owner.state.counter).toBe(0);
@@ -1381,10 +1482,10 @@ describe("t-on", () => {
let owner = { let owner = {
state: { state: {
counter: 0, counter: 0,
incrementCounter: inc => { incrementCounter: (inc) => {
owner.state.counter += inc; owner.state.counter += inc;
} },
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.counter).toBe(0); expect(owner.state.counter).toBe(0);
@@ -1396,8 +1497,8 @@ describe("t-on", () => {
qweb.addTemplate("test", `<button t-on-click="state.flag = !state.flag">Toggle</button>`); qweb.addTemplate("test", `<button t-on-click="state.flag = !state.flag">Toggle</button>`);
let owner = { let owner = {
state: { state: {
flag: true flag: true,
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.flag).toBe(true); expect(owner.state.flag).toBe(true);
@@ -1414,8 +1515,8 @@ describe("t-on", () => {
return n + 1; return n + 1;
}, },
state: { state: {
n: 11 n: 11,
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.n).toBe(11); expect(owner.state.n).toBe(11);
@@ -1431,7 +1532,7 @@ describe("t-on", () => {
let owner = { let owner = {
update() { update() {
expect(this).toBe(owner); expect(this).toBe(owner);
} },
}; };
const node = renderToDOM(qweb, "main", owner, { handlers: [] }); const node = renderToDOM(qweb, "main", owner, { handlers: [] });
@@ -1448,7 +1549,7 @@ describe("t-on", () => {
expect(this).toBe(owner); expect(this).toBe(owner);
expect(val).toBe(444); expect(val).toBe(444);
}, },
value: 444 value: 444,
}; };
const node = renderToDOM(qweb, "main", owner, { handlers: [] }); const node = renderToDOM(qweb, "main", owner, { handlers: [] });
@@ -1477,7 +1578,7 @@ describe("t-on", () => {
onClickPreventedAndStopped(e) { onClickPreventedAndStopped(e) {
expect(e.defaultPrevented).toBe(true); expect(e.defaultPrevented).toBe(true);
expect(e.cancelBubble).toBe(true); expect(e.cancelBubble).toBe(true);
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1503,7 +1604,7 @@ describe("t-on", () => {
}, },
onClickSelf(e) { onClickSelf(e) {
steps.push("onClickSelf"); steps.push("onClickSelf");
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1527,10 +1628,10 @@ describe("t-on", () => {
); );
let steps: boolean[] = []; let steps: boolean[] = [];
let owner = { let owner = {
onClick() {} onClick() {},
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
(<HTMLElement>node).addEventListener("click", function(e) { (<HTMLElement>node).addEventListener("click", function (e) {
steps.push(e.defaultPrevented); steps.push(e.defaultPrevented);
}); });
@@ -1552,10 +1653,10 @@ describe("t-on", () => {
); );
let steps: boolean[] = []; let steps: boolean[] = [];
let owner = { let owner = {
onClick() {} onClick() {},
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
(<HTMLElement>node).addEventListener("click", function(e) { (<HTMLElement>node).addEventListener("click", function (e) {
steps.push(e.defaultPrevented); steps.push(e.defaultPrevented);
}); });
@@ -1583,13 +1684,13 @@ describe("t-on", () => {
const owner = { const owner = {
projects: [ projects: [
{ id: 1, name: "Project 1" }, { id: 1, name: "Project 1" },
{ id: 2, name: "Project 2" } { id: 2, name: "Project 2" },
], ],
onEdit(projectId, ev) { onEdit(projectId, ev) {
expect(ev.defaultPrevented).toBe(true); expect(ev.defaultPrevented).toBe(true);
steps.push(projectId); steps.push(projectId);
} },
}; };
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] }); const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1614,7 +1715,7 @@ describe("t-on", () => {
); );
const node = renderToDOM(qweb, "test", {}, { handlers: [] }); const node = renderToDOM(qweb, "test", {}, { handlers: [] });
node.addEventListener("click", e => { node.addEventListener("click", (e) => {
expect(e.defaultPrevented).toBe(true); expect(e.defaultPrevented).toBe(true);
}); });
@@ -1630,7 +1731,7 @@ describe("t-on", () => {
text: "Click here", text: "Click here",
onClick() { onClick() {
steps.push("onClick"); steps.push("onClick");
} },
}; };
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] }); const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1649,7 +1750,7 @@ describe("t-on", () => {
html: "Click <b>here</b>", html: "Click <b>here</b>",
onClick() { onClick() {
steps.push("onClick"); steps.push("onClick");
} },
}; };
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] }); const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1676,7 +1777,7 @@ describe("t-on", () => {
}, },
doSomething() { doSomething() {
steps.push("normal"); steps.push("normal");
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1749,7 +1850,7 @@ describe("loading templates", () => {
}); });
}); });
describe("special cases for some boolean html attributes/properties", () => { describe("special cases for some specific html attributes/properties", () => {
test("input type= checkbox, with t-att-checked", () => { test("input type= checkbox, with t-att-checked", () => {
qweb.addTemplate("test", `<input type="checkbox" t-att-checked="flag"/>`); qweb.addTemplate("test", `<input type="checkbox" t-att-checked="flag"/>`);
const result = renderToString(qweb, "test", { flag: true }); const result = renderToString(qweb, "test", { flag: true });
@@ -1776,6 +1877,33 @@ describe("special cases for some boolean html attributes/properties", () => {
); );
renderToString(qweb, "test", { flag: true }); renderToString(qweb, "test", { flag: true });
}); });
test("input with t-att-value", () => {
// render input with initial value
qweb.addTemplate("test", `<input t-att-value="v"/>`);
const vnode1 = qweb.render("test", { v: "zucchini" });
const vnode2 = patch(document.createElement("input"), vnode1);
let elm = vnode2.elm as HTMLInputElement;
expect(elm.value).toBe("zucchini");
// change value manually in input, to simulate user input
elm.value = "tomato";
expect(elm.value).toBe("tomato");
// rerender with a different value, and patch actual dom, to check that
// input value was properly reset by owl
const vnode3 = qweb.render("test", { v: "potato" });
patch(vnode2, vnode3);
expect(elm.value).toBe("potato");
});
test("input of type checkbox with t-att-indeterminate", () => {
qweb.addTemplate("test", `<input type="checkbox" t-att-indeterminate="v"/>`);
const vnode1 = qweb.render("test", { v: true });
const vnode2 = patch(document.createElement("input"), vnode1);
let elm = vnode2.elm as HTMLInputElement;
expect(elm.indeterminate).toBe(true);
});
}); });
describe("whitespace handling", () => { describe("whitespace handling", () => {
@@ -1840,7 +1968,7 @@ describe("t-key", () => {
); );
expect( expect(
renderToString(qweb, "test", { renderToString(qweb, "test", {
beers: [{ id: 12, name: "Chimay Rouge" }] beers: [{ id: 12, name: "Chimay Rouge" }],
}) })
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
@@ -1953,9 +2081,9 @@ describe("properly support svg", () => {
describe("translation support", () => { describe("translation support", () => {
test("can translate node content", () => { test("can translate node content", () => {
const translations = { const translations = {
word: "mot" word: "mot",
}; };
const translateFn = expr => translations[expr] || expr; const translateFn = (expr) => translations[expr] || expr;
const qweb = new QWeb({ translateFn }); const qweb = new QWeb({ translateFn });
qweb.addTemplate("test", "<div>word</div>"); qweb.addTemplate("test", "<div>word</div>");
expect(renderToString(qweb, "test")).toBe("<div>mot</div>"); expect(renderToString(qweb, "test")).toBe("<div>mot</div>");
@@ -1963,9 +2091,9 @@ describe("translation support", () => {
test("does not translate node content if disabled", () => { test("does not translate node content if disabled", () => {
const translations = { const translations = {
word: "mot" word: "mot",
}; };
const translateFn = expr => translations[expr] || expr; const translateFn = (expr) => translations[expr] || expr;
const qweb = new QWeb({ translateFn }); const qweb = new QWeb({ translateFn });
qweb.addTemplate( qweb.addTemplate(
"test", "test",
@@ -1980,9 +2108,9 @@ describe("translation support", () => {
test("some attributes are translated", () => { test("some attributes are translated", () => {
const translations = { const translations = {
word: "mot" word: "mot",
}; };
const translateFn = expr => translations[expr] || expr; const translateFn = (expr) => translations[expr] || expr;
const qweb = new QWeb({ translateFn }); const qweb = new QWeb({ translateFn });
qweb.addTemplate( qweb.addTemplate(
"test", "test",
+10 -10
View File
@@ -6,12 +6,12 @@ describe("tokenizer", () => {
expect(tokenize("{}")).toEqual([ expect(tokenize("{}")).toEqual([
{ type: "LEFT_BRACE", value: "{" }, { type: "LEFT_BRACE", value: "{" },
{ type: "RIGHT_BRACE", value: "}" } { type: "RIGHT_BRACE", value: "}" },
]); ]);
expect(tokenize("{ }}")).toEqual([ expect(tokenize("{ }}")).toEqual([
{ type: "LEFT_BRACE", value: "{" }, { type: "LEFT_BRACE", value: "{" },
{ type: "RIGHT_BRACE", value: "}" }, { type: "RIGHT_BRACE", value: "}" },
{ type: "RIGHT_BRACE", value: "}" } { type: "RIGHT_BRACE", value: "}" },
]); ]);
expect(tokenize("a")).toEqual([{ type: "SYMBOL", value: "a" }]); expect(tokenize("a")).toEqual([{ type: "SYMBOL", value: "a" }]);
expect(tokenize("true")).toEqual([{ type: "SYMBOL", value: "true" }]); expect(tokenize("true")).toEqual([{ type: "SYMBOL", value: "true" }]);
@@ -25,15 +25,15 @@ describe("tokenizer", () => {
{ type: "SYMBOL", value: "a" }, { type: "SYMBOL", value: "a" },
{ type: "COLON", value: ":" }, { type: "COLON", value: ":" },
{ type: "VALUE", value: "2" }, { type: "VALUE", value: "2" },
{ type: "RIGHT_BRACE", value: "}" } { type: "RIGHT_BRACE", value: "}" },
]); ]);
expect(tokenize("a,")).toEqual([ expect(tokenize("a,")).toEqual([
{ type: "SYMBOL", value: "a" }, { type: "SYMBOL", value: "a" },
{ type: "COMMA", value: "," } { type: "COMMA", value: "," },
]); ]);
expect(tokenize("][")).toEqual([ expect(tokenize("][")).toEqual([
{ type: "RIGHT_BRACKET", value: "]" }, { type: "RIGHT_BRACKET", value: "]" },
{ type: "LEFT_BRACKET", value: "[" } { type: "LEFT_BRACKET", value: "[" },
]); ]);
}); });
@@ -44,23 +44,23 @@ describe("tokenizer", () => {
{ type: "OPERATOR", value: "<" }, { type: "OPERATOR", value: "<" },
{ type: "OPERATOR", value: ">" }, { type: "OPERATOR", value: ">" },
{ type: "OPERATOR", value: "!==" }, { type: "OPERATOR", value: "!==" },
{ type: "OPERATOR", value: "!=" } { type: "OPERATOR", value: "!=" },
]); ]);
expect(tokenize("typeof a")).toEqual([ expect(tokenize("typeof a")).toEqual([
{ type: "OPERATOR", value: "typeof " }, { type: "OPERATOR", value: "typeof " },
{ type: "SYMBOL", value: "a" } { type: "SYMBOL", value: "a" },
]); ]);
expect(tokenize("a...1")).toEqual([ expect(tokenize("a...1")).toEqual([
{ type: "SYMBOL", value: "a" }, { type: "SYMBOL", value: "a" },
{ type: "OPERATOR", value: "..." }, { type: "OPERATOR", value: "..." },
{ type: "VALUE", value: "1" } { type: "VALUE", value: "1" },
]); ]);
expect(tokenize("a in b")).toEqual([ expect(tokenize("a in b")).toEqual([
{ type: "SYMBOL", value: "a" }, { type: "SYMBOL", value: "a" },
{ type: "OPERATOR", value: "in " }, { type: "OPERATOR", value: "in " },
{ type: "SYMBOL", value: "b" } { type: "SYMBOL", value: "b" },
]); ]);
}); });
@@ -76,7 +76,7 @@ describe("tokenizer", () => {
expect(tokenize('"hello ged"')).toEqual([{ type: "VALUE", value: '"hello ged"' }]); expect(tokenize('"hello ged"')).toEqual([{ type: "VALUE", value: '"hello ged"' }]);
expect(tokenize('"hello ged"}')).toEqual([ expect(tokenize('"hello ged"}')).toEqual([
{ type: "VALUE", value: '"hello ged"' }, { type: "VALUE", value: '"hello ged"' },
{ type: "RIGHT_BRACE", value: "}" } { type: "RIGHT_BRACE", value: "}" },
]); ]);
expect(tokenize('"hello \\"ged\\""')).toEqual([{ type: "VALUE", value: '"hello \\"ged\\""' }]); expect(tokenize('"hello \\"ged\\""')).toEqual([{ type: "VALUE", value: '"hello \\"ged\\""' }]);
}); });
+3 -3
View File
@@ -37,7 +37,7 @@ describe("Link component", () => {
const routes = [ const routes = [
{ name: "about", path: "/about" }, { name: "about", path: "/about" },
{ name: "users", path: "/users" } { name: "users", path: "/users" },
]; ];
router = new TestRouter(env, routes, { mode: "history" }); router = new TestRouter(env, routes, { mode: "history" });
@@ -71,7 +71,7 @@ describe("Link component", () => {
const routes = [ const routes = [
{ name: "about", path: "/about" }, { name: "about", path: "/about" },
{ name: "users", path: "/users" } { name: "users", path: "/users" },
]; ];
router = new TestRouter(env, routes, { mode: "history" }); router = new TestRouter(env, routes, { mode: "history" });
@@ -81,7 +81,7 @@ describe("Link component", () => {
expect(window.location.pathname).toBe("/users"); expect(window.location.pathname).toBe("/users");
var evt = new MouseEvent("click", { var evt = new MouseEvent("click", {
button: 1 button: 1,
}); });
fixture.querySelector("a")!.dispatchEvent(evt); fixture.querySelector("a")!.dispatchEvent(evt);
+1 -1
View File
@@ -41,7 +41,7 @@ describe("RouteComponent", () => {
const routes = [ const routes = [
{ name: "about", path: "/about", component: About }, { name: "about", path: "/about", component: About },
{ name: "users", path: "/users", component: Users } { name: "users", path: "/users", component: Users },
]; ];
router = new TestRouter(env, routes, { mode: "history" }); router = new TestRouter(env, routes, { mode: "history" });
+10 -10
View File
@@ -21,7 +21,7 @@ describe("router miscellaneous", () => {
test("validate routes shape", () => { test("validate routes shape", () => {
expect(() => { expect(() => {
router = new TestRouter(env, [ router = new TestRouter(env, [
{ name: "someroute", path: "/some/path", redirect: { abc: "hey" } as Destination } { name: "someroute", path: "/some/path", redirect: { abc: "hey" } as Destination },
]); ]);
}).toThrow(`Invalid destination: {"abc":"hey"}`); }).toThrow(`Invalid destination: {"abc":"hey"}`);
}); });
@@ -133,7 +133,7 @@ describe("getRouteParams", () => {
test("match some parameterized routes", () => { test("match some parameterized routes", () => {
router = new TestRouter(env, []); router = new TestRouter(env, []);
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "/invoices/3")).toEqual({ expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "/invoices/3")).toEqual({
id: "3" id: "3",
}); });
}); });
@@ -141,7 +141,7 @@ describe("getRouteParams", () => {
router = new TestRouter(env, [], { mode: "hash" }); router = new TestRouter(env, [], { mode: "hash" });
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "#/invoices/3")).toEqual( expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "#/invoices/3")).toEqual(
{ {
id: "3" id: "3",
} }
); );
}); });
@@ -151,7 +151,7 @@ describe("getRouteParams", () => {
expect( expect(
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "/invoices/3") router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "/invoices/3")
).toEqual({ ).toEqual({
id: 3 id: 3,
}); });
}); });
@@ -160,7 +160,7 @@ describe("getRouteParams", () => {
expect( expect(
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "#/invoices/3") router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "#/invoices/3")
).toEqual({ ).toEqual({
id: 3 id: 3,
}); });
}); });
}); });
@@ -169,7 +169,7 @@ describe("redirect", () => {
test("can redirect to other route", async () => { test("can redirect to other route", async () => {
router = new TestRouter(env, [ router = new TestRouter(env, [
{ name: "routea", path: "/some/path" }, { name: "routea", path: "/some/path" },
{ name: "routeb", path: "/some/other/path", redirect: { to: "routea" } } { name: "routeb", path: "/some/other/path", redirect: { to: "routea" } },
]); ]);
await router.start(); await router.start();
@@ -182,7 +182,7 @@ describe("redirect", () => {
test("can redirect to other path", async () => { test("can redirect to other path", async () => {
router = new TestRouter(env, [ router = new TestRouter(env, [
{ name: "routea", path: "/some/path" }, { name: "routea", path: "/some/path" },
{ name: "routeb", path: "/some/other/path", redirect: { path: "/some/path" } } { name: "routeb", path: "/some/other/path", redirect: { path: "/some/path" } },
]); ]);
router.navigate = jest.fn(router.navigate); router.navigate = jest.fn(router.navigate);
@@ -214,7 +214,7 @@ describe("beforeRouteEnter", () => {
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();
@@ -235,7 +235,7 @@ describe("beforeRouteEnter", () => {
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();
@@ -255,7 +255,7 @@ describe("beforeRouteEnter", () => {
}); });
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("/");
+28 -28
View File
@@ -8,7 +8,7 @@ describe("basic use", () => {
const actions = { const actions = {
inc({ state }, delta) { inc({ state }, delta) {
state.n += delta; state.n += delta;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -25,7 +25,7 @@ describe("basic use", () => {
state.n1 += delta1; state.n1 += delta1;
state.n2 += delta2; state.n2 += delta2;
state.n3 += delta3; state.n3 += delta3;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -47,7 +47,7 @@ describe("basic use", () => {
}, },
inc100({ dispatch }) { inc100({ dispatch }) {
dispatch("inc", 100); dispatch("inc", 100);
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -61,7 +61,7 @@ describe("basic use", () => {
const actions = { const actions = {
inc({ state }) { inc({ state }) {
return ++state.n; return ++state.n;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -87,7 +87,7 @@ describe("basic use", () => {
async setTo10({ dispatch }) { async setTo10({ dispatch }) {
await Promise.resolve(); await Promise.resolve();
dispatch("setN", 10); dispatch("setN", 10);
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -115,7 +115,7 @@ describe("basic use", () => {
await Promise.resolve(); await Promise.resolve();
dispatch("setN", 10); dispatch("setN", 10);
return 5; return 5;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -132,7 +132,7 @@ describe("basic use", () => {
const actions = { const actions = {
someaction({ env }) { someaction({ env }) {
expect(env).toBe(someEnv); expect(env).toBe(someEnv);
} },
}; };
const store = new Store({ state: {}, actions, env: someEnv }); const store = new Store({ state: {}, actions, env: someEnv });
@@ -145,15 +145,15 @@ describe("basic use", () => {
1: { 1: {
id: 1, id: 1,
name: "bertinchamps", name: "bertinchamps",
tasterID: 1 tasterID: 1,
} },
}, },
tasters: { tasters: {
1: { 1: {
id: 1, id: 1,
name: "aaron" name: "aaron",
} },
} },
}; };
const getters: { [key: string]: Getter } = { const getters: { [key: string]: Getter } = {
beerTasterName({ state }, beerID) { beerTasterName({ state }, beerID) {
@@ -161,7 +161,7 @@ describe("basic use", () => {
}, },
bestBeerName({ state }) { bestBeerName({ state }) {
return state.beers[1].name; return state.beers[1].name;
} },
}; };
const store = new Store({ state, actions: {}, getters }); const store = new Store({ state, actions: {}, getters });
expect(store.getters).toBeDefined(); expect(store.getters).toBeDefined();
@@ -176,15 +176,15 @@ describe("basic use", () => {
1: { 1: {
id: 1, id: 1,
name: "bertinchamps", name: "bertinchamps",
tasterID: 1 tasterID: 1,
} },
}, },
tasters: { tasters: {
1: { 1: {
id: 1, id: 1,
name: "aaron" name: "aaron",
} },
} },
}; };
const getters = { const getters = {
beerTasterName({ state }, beerID) { beerTasterName({ state }, beerID) {
@@ -192,14 +192,14 @@ describe("basic use", () => {
}, },
bestBeerName({ state }) { bestBeerName({ state }) {
return state.beers[1].name; return state.beers[1].name;
} },
}; };
const actions = { const actions = {
action({ getters }) { action({ getters }) {
expect(getters).toBeDefined(); expect(getters).toBeDefined();
expect(getters.bestBeerName()).toBe("bertinchamps"); expect(getters.bestBeerName()).toBe("bertinchamps");
expect(getters.beerTasterName(1)).toBe("aaron"); expect(getters.beerTasterName(1)).toBe("aaron");
} },
}; };
const store = new Store({ state, actions, getters }); const store = new Store({ state, actions, getters });
store.dispatch("action"); store.dispatch("action");
@@ -215,7 +215,7 @@ describe("basic use", () => {
}, },
c({}, i) { c({}, i) {
return `c${i}`; return `c${i}`;
} },
}; };
const store = new Store({ getters, state: {}, actions: {} }); const store = new Store({ getters, state: {}, actions: {} });
@@ -226,7 +226,7 @@ describe("basic use", () => {
describe("advanced state properties", () => { describe("advanced state properties", () => {
test("state in the store is reference equal after mutation", async () => { test("state in the store is reference equal after mutation", async () => {
const actions = { const actions = {
donothing() {} donothing() {},
}; };
const store = new Store({ state: {}, actions }); const store = new Store({ state: {}, actions });
const state = store.state; const state = store.state;
@@ -242,7 +242,7 @@ describe("advanced state properties", () => {
expect(state.a.length).toBe(3); expect(state.a.length).toBe(3);
const l = state.a.push(53); const l = state.a.push(53);
expect(l).toBe(4); expect(l).toBe(4);
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
store.dispatch("m"); store.dispatch("m");
@@ -253,11 +253,11 @@ describe("advanced state properties", () => {
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"); store.dispatch("dosomething");
expect(store.state.westmalle).toEqual({ a: 3, b: 4 }); expect(store.state.westmalle).toEqual({ a: 3, b: 4 });
@@ -267,7 +267,7 @@ describe("advanced state properties", () => {
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 });
@@ -284,7 +284,7 @@ describe("updates triggered by the store", () => {
const actions = { const actions = {
inc({ state }, delta) { inc({ state }, delta) {
state.n += delta; state.n += delta;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
store.on("update", null, () => updateCounter++); store.on("update", null, () => updateCounter++);
@@ -308,7 +308,7 @@ describe("updates triggered by the store", () => {
noop2({ state }) { noop2({ state }) {
const val = state.n; const val = state.n;
state.n = val; state.n = val;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
store.on("update", null, () => updateCounter++); store.on("update", null, () => updateCounter++);
+90 -90
View File
@@ -25,7 +25,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
addTodo({ state }, msg) { addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ }); state.todos.push({ msg, id: nextId++ });
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -34,7 +34,7 @@ describe("connecting a component to store", () => {
<div> <div>
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span> <span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
</div>`; </div>`;
todos = useStore(state => state.todos); todos = useStore((state) => state.todos);
} }
(<any>env).store = store; (<any>env).store = store;
@@ -56,7 +56,7 @@ describe("connecting a component to store", () => {
}, },
setNotNull({ state }) { setNotNull({ state }) {
state.nullValue = "ok"; state.nullValue = "ok";
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -70,15 +70,15 @@ describe("connecting a component to store", () => {
nullValue: string; nullValue: string;
constructor() { constructor() {
super(); super();
this.isBoolean = useStore(state => state.isBoolean, { this.isBoolean = useStore((state) => state.isBoolean, {
onUpdate: isBoolean => { onUpdate: (isBoolean) => {
this.isBoolean = isBoolean; this.isBoolean = isBoolean;
} },
}); });
this.nullValue = useStore(state => state.nullValue, { this.nullValue = useStore((state) => state.nullValue, {
onUpdate: nullValue => { onUpdate: (nullValue) => {
this.nullValue = nullValue; this.nullValue = nullValue;
} },
}); });
} }
} }
@@ -105,15 +105,15 @@ describe("connecting a component to store", () => {
class App extends Component { class App extends Component {
static template = xml`<div t-esc="mapAdd"/>`; static template = xml`<div t-esc="mapAdd"/>`;
storeProps = { storeProps = {
array: useStore(state => { array: useStore((state) => {
if (state.useSmallArray) { if (state.useSmallArray) {
return state.smallerArray; return state.smallerArray;
} }
return state.biggerArray; return state.biggerArray;
}) }),
}; };
get mapAdd() { get mapAdd() {
return this.storeProps.array.map(a => { return this.storeProps.array.map((a) => {
return a + 1; return a + 1;
}); });
} }
@@ -133,7 +133,7 @@ describe("connecting a component to store", () => {
test("throw error if no store is found", async () => { test("throw error if no store is found", async () => {
class App extends Component { class App extends Component {
static template = xml`<div></div>`; static template = xml`<div></div>`;
todos = useStore(state => state.todos); todos = useStore((state) => state.todos);
} }
let error; let error;
@@ -153,7 +153,7 @@ describe("connecting a component to store", () => {
class App extends Component { class App extends Component {
static template = xml`<div/>`; static template = xml`<div/>`;
storeState = useStore(state => state.a); storeState = useStore((state) => state.a);
} }
(<any>env).store = store; (<any>env).store = store;
@@ -170,7 +170,7 @@ describe("connecting a component to store", () => {
doSomething({ state }) { doSomething({ state }) {
state.a = 2; state.a = 2;
state.b = 3; state.b = 3;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -180,8 +180,8 @@ describe("connecting a component to store", () => {
<span t-esc="a.value"/> <span t-esc="a.value"/>
<span t-esc="b.value"/> <span t-esc="b.value"/>
</div>`; </div>`;
a = useStore(state => ({ value: state.a })); a = useStore((state) => ({ value: state.a }));
b = useStore(state => ({ value: state.b })); b = useStore((state) => ({ value: state.b }));
} }
App.prototype.__render = jest.fn(App.prototype.__render); App.prototype.__render = jest.fn(App.prototype.__render);
@@ -204,7 +204,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
addTodo({ state }, msg) { addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ }); state.todos.push({ msg, id: nextId++ });
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -213,7 +213,7 @@ describe("connecting a component to store", () => {
<div> <div>
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span> <span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
</div>`; </div>`;
todos = useStore(state => state.todos); todos = useStore((state) => state.todos);
} }
App.prototype.__render = jest.fn(App.prototype.__render); App.prototype.__render = jest.fn(App.prototype.__render);
@@ -241,13 +241,13 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
addTodo({ state }, msg) { addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ }); state.todos.push({ msg, id: nextId++ });
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
class App extends Component { class App extends Component {
static template = xml`<div><t t-esc="nbrTodos.value"/></div>`; static template = xml`<div><t t-esc="nbrTodos.value"/></div>`;
nbrTodos = useStore(state => ({ value: state.todos.length })); nbrTodos = useStore((state) => ({ value: state.todos.length }));
} }
(<any>env).store = store; (<any>env).store = store;
@@ -270,12 +270,12 @@ describe("connecting a component to store", () => {
}, },
incrementA({ state }) { incrementA({ state }) {
state.a++; state.a++;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
class App extends Component { class App extends Component {
static template = xml`<div><t t-esc="nbrTodos.value"/></div>`; static template = xml`<div><t t-esc="nbrTodos.value"/></div>`;
nbrTodos = useStore(state => ({ value: state.todos.length }), { isEqual: shallowEqual }); nbrTodos = useStore((state) => ({ value: state.todos.length }), { isEqual: shallowEqual });
} }
App.prototype.__render = jest.fn(App.prototype.__render); App.prototype.__render = jest.fn(App.prototype.__render);
@@ -303,7 +303,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
addTodo({ state }, msg) { addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ }); state.todos.push({ msg, id: nextId++ });
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -312,7 +312,7 @@ describe("connecting a component to store", () => {
<div> <div>
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span> <span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
</div>`; </div>`;
todos = useStore(state => state.todos, { store }); todos = useStore((state) => state.todos, { store });
} }
const app = new App(); const app = new App();
@@ -331,8 +331,8 @@ describe("connecting a component to store", () => {
actions: { actions: {
inc({ state }) { inc({ state }) {
state.value++; state.value++;
} },
} },
}); });
(<any>env).store = store; (<any>env).store = store;
@@ -342,7 +342,7 @@ describe("connecting a component to store", () => {
<button t-on-click="dispatch('inc')">Inc</button> <button t-on-click="dispatch('inc')">Inc</button>
<span><t t-esc="storeState.value"/></span> <span><t t-esc="storeState.value"/></span>
</div>`; </div>`;
storeState = useStore(state => state); storeState = useStore((state) => state);
dispatch = useDispatch(); dispatch = useDispatch();
} }
const app = new App(); const app = new App();
@@ -360,15 +360,15 @@ describe("connecting a component to store", () => {
const state = { const state = {
todos: [ todos: [
{ id: 1, text: "jupiler" }, { id: 1, text: "jupiler" },
{ id: 2, text: "chimay" } { id: 2, text: "chimay" },
] ],
}; };
const store = new Store({ state, actions: {} }); const store = new Store({ state, actions: {} });
class TodoItem extends Component { class TodoItem extends Component {
static template = xml`<span><t t-esc="todo.text"/></span>`; static template = xml`<span><t t-esc="todo.text"/></span>`;
todo = useStore((state, props) => { todo = useStore((state, props) => {
return state.todos.find(t => t.id === props.todoId); return state.todos.find((t) => t.id === props.todoId);
}); });
} }
@@ -395,14 +395,14 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
addTodo({ state }, text) { addTodo({ state }, text) {
state.todos.push({ text, id: nextId++ }); state.todos.push({ text, id: nextId++ });
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
class TodoItem extends Component { class TodoItem extends Component {
static template = xml`<span><t t-esc="todo.text"/></span>`; static template = xml`<span><t t-esc="todo.text"/></span>`;
todo = useStore((state, props) => { todo = useStore((state, props) => {
return state.todos.find(t => t.id === props.id); return state.todos.find((t) => t.id === props.id);
}); });
} }
@@ -412,7 +412,7 @@ describe("connecting a component to store", () => {
<TodoItem t-foreach="todos" t-as="todo" id="todo.id" t-key="todo.id"/> <TodoItem t-foreach="todos" t-as="todo" id="todo.id" t-key="todo.id"/>
</div>`; </div>`;
static components = { TodoItem }; static components = { TodoItem };
todos = useStore(state => state.todos); todos = useStore((state) => state.todos);
} }
(<any>env).store = store; (<any>env).store = store;
@@ -431,16 +431,16 @@ describe("connecting a component to store", () => {
importantID: 1, importantID: 1,
todos: [ todos: [
{ id: 1, text: "jupiler" }, { id: 1, text: "jupiler" },
{ id: 2, text: "bertinchamps" } { id: 2, text: "bertinchamps" },
] ],
}; };
const getters = { const getters = {
importantTodoText({ state }) { importantTodoText({ state }) {
return state.todos.find(todo => todo.id === state.importantID).text; return state.todos.find((todo) => todo.id === state.importantID).text;
}, },
text({ state }, id) { text({ state }, id) {
return state.todos.find(todo => todo.id === id).text; return state.todos.find((todo) => todo.id === id).text;
} },
}; };
const store = new Store({ state, getters }); const store = new Store({ state, getters });
@@ -452,10 +452,10 @@ describe("connecting a component to store", () => {
</div>`; </div>`;
getters = useGetters(); getters = useGetters();
storeProps = useStore((state, props) => { storeProps = useStore((state, props) => {
const todo = state.todos.find(t => t.id === props.id); const todo = state.todos.find((t) => t.id === props.id);
return { return {
activeTodoText: this.getters.text(todo.id), activeTodoText: this.getters.text(todo.id),
importantTodoText: this.getters.importantTodoText() importantTodoText: this.getters.importantTodoText(),
}; };
}); });
} }
@@ -468,7 +468,7 @@ describe("connecting a component to store", () => {
<TodoItem id="todo.id" t-key="todo.id"/> <TodoItem id="todo.id" t-key="todo.id"/>
</t> </t>
</div>`; </div>`;
todos = useStore(state => state.todos); todos = useStore((state) => state.todos);
} }
(<any>env).store = store; (<any>env).store = store;
@@ -513,13 +513,13 @@ describe("connecting a component to store", () => {
</div>`; </div>`;
// we have here a new object // we have here a new object
data = useStore(state => ({ beers: state.beers, otherKey: 1 })); data = useStore((state) => ({ beers: state.beers, otherKey: 1 }));
} }
const actions = { const actions = {
addBeer({ state }, name) { addBeer({ state }, name) {
state.beers.push({ name }); state.beers.push({ name });
} },
}; };
const state = { beers: [{ name: "jupiler" }] }; const state = { beers: [{ name: "jupiler" }] };
@@ -542,9 +542,9 @@ describe("connecting a component to store", () => {
class Beer extends Component { class Beer extends Component {
static template = xml`<span><t t-esc="beer.name"/></span>`; static template = xml`<span><t t-esc="beer.name"/></span>`;
beer = useStore((state, props) => state.beers[props.id], { beer = useStore((state, props) => state.beers[props.id], {
onUpdate: result => { onUpdate: (result) => {
++counter; ++counter;
} },
}); });
} }
@@ -558,7 +558,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
renameBeer({ state }, { id, name }) { renameBeer({ state }, { id, name }) {
state.beers[id].name = name; state.beers[id].name = name;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
(<any>env).store = store; (<any>env).store = store;
@@ -609,7 +609,7 @@ describe("connecting a component to store", () => {
data = useStore((state, props) => ({ data = useStore((state, props) => ({
selected: state.beers[props.id], selected: state.beers[props.id],
consumed: state.beers[state.consumedID] || null, consumed: state.beers[state.consumedID] || null,
taster: state.taster taster: state.taster,
})); }));
} }
@@ -622,14 +622,14 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
consume({ state }, beerId) { consume({ state }, beerId) {
state.consumedID = beerId; state.consumedID = beerId;
} },
}; };
const state = { const state = {
beers: { beers: {
1: { name: "jupiler" } 1: { name: "jupiler" },
}, },
consumedID: null, consumedID: null,
taster: "aaron" taster: "aaron",
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
(<any>env).store = store; (<any>env).store = store;
@@ -665,11 +665,11 @@ describe("connecting a component to store", () => {
<span t-if="info.selected">selected:<t t-esc="info.selected.name"/></span> <span t-if="info.selected">selected:<t t-esc="info.selected.name"/></span>
<span t-if="info.consumed">consumed:<t t-esc="info.consumed.name"/></span> <span t-if="info.consumed">consumed:<t t-esc="info.consumed.name"/></span>
</div>`; </div>`;
info = useStore(function(state, props) { info = useStore(function (state, props) {
return { return {
selected: state.beers[props.id], selected: state.beers[props.id],
consumed: state.beers[state.consumedID] || null, consumed: state.beers[state.consumedID] || null,
taster: state.taster taster: state.taster,
}; };
}); });
} }
@@ -689,14 +689,14 @@ describe("connecting a component to store", () => {
}, },
renameBeer({ state }, { beerId, name }) { renameBeer({ state }, { beerId, name }) {
state.beers[beerId].name = name; state.beers[beerId].name = name;
} },
}; };
const state = { const state = {
beers: { beers: {
1: { name: "jupiler" } 1: { name: "jupiler" },
}, },
consumedID: null, consumedID: null,
taster: "aaron" taster: "aaron",
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
(<any>env).store = store; (<any>env).store = store;
@@ -747,11 +747,11 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
setValue({ state }, val) { setValue({ state }, val) {
state.x.val = val; state.x.val = val;
} },
}; };
class Parent extends Component { class Parent extends Component {
static template = xml`<div><t t-esc="x.val"/></div>`; static template = xml`<div><t t-esc="x.val"/></div>`;
x = useStore(state => { x = useStore((state) => {
return Object.assign({}, state.x); return Object.assign({}, state.x);
}); });
dispatch = useDispatch(); dispatch = useDispatch();
@@ -782,11 +782,11 @@ describe("connecting a component to store", () => {
static template = xml`<div><Child key="state.current"/></div>`; static template = xml`<div><Child key="state.current"/></div>`;
static components = { Child }; static components = { Child };
state = useStore(state => { state = useStore((state) => {
steps.push("parent"); steps.push("parent");
return { return {
current: state.current, current: state.current,
isvisible: state.isvisible isvisible: state.isvisible,
}; };
}); });
} }
@@ -795,7 +795,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
setCurrent({ state }, c) { setCurrent({ state }, c) {
state.current = c; state.current = c;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -831,7 +831,7 @@ describe("connecting a component to store", () => {
<Child t-if="state.flag" someId="state.someId"/> <Child t-if="state.flag" someId="state.someId"/>
</div>`; </div>`;
static components = { Child }; static components = { Child };
state = useStore(s => { state = useStore((s) => {
steps.push("parent"); steps.push("parent");
return { flag: s.flag, someId: s.someId }; return { flag: s.flag, someId: s.someId };
}); });
@@ -846,7 +846,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
setFlagToFalse({ state }) { setFlagToFalse({ state }) {
state.flag = false; state.flag = false;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -874,15 +874,15 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
editTodo({ state }) { editTodo({ state }) {
state.todos[1].title = "abc"; state.todos[1].title = "abc";
} },
}; };
const todos = { 1: { id: 1, title: "kikoou" } }; const todos = { 1: { id: 1, title: "kikoou" } };
const state = { const state = {
todos todos,
}; };
const store = new Store({ const store = new Store({
state, state,
actions actions,
}); });
class TodoItem extends Component<{}, EnvWithStore> { class TodoItem extends Component<{}, EnvWithStore> {
@@ -894,7 +894,7 @@ describe("connecting a component to store", () => {
state = useStore((state, props) => { state = useStore((state, props) => {
steps.push("item:usestore"); steps.push("item:usestore");
return { return {
todo: state.todos[props.id] todo: state.todos[props.id],
}; };
}); });
@@ -914,7 +914,7 @@ describe("connecting a component to store", () => {
</t> </t>
</div>`; </div>`;
static components = { TodoItem }; static components = { TodoItem };
state = useStore(state => { state = useStore((state) => {
steps.push("app:usestore"); steps.push("app:usestore");
return { todos: state.todos }; return { todos: state.todos };
}); });
@@ -947,15 +947,15 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
removeTodo({ state }) { removeTodo({ state }) {
delete state.todos[1]; delete state.todos[1];
} },
}; };
const todos = { 1: { id: 1, title: "kikoou" } }; const todos = { 1: { id: 1, title: "kikoou" } };
const state = { const state = {
todos todos,
}; };
const store = new Store({ const store = new Store({
state, state,
actions actions,
}); });
class TodoItem extends Component<{}, EnvWithStore> { class TodoItem extends Component<{}, EnvWithStore> {
@@ -967,7 +967,7 @@ describe("connecting a component to store", () => {
state = useStore((state, props) => { state = useStore((state, props) => {
steps.push("item:usestore"); steps.push("item:usestore");
return { return {
todo: state.todos[props.id] todo: state.todos[props.id],
}; };
}); });
@@ -988,7 +988,7 @@ describe("connecting a component to store", () => {
</t> </t>
</div>`; </div>`;
static components = { TodoItem }; static components = { TodoItem };
state = useStore(state => { state = useStore((state) => {
steps.push("app:usestore"); steps.push("app:usestore");
return { todos: state.todos }; return { todos: state.todos };
}); });
@@ -1016,7 +1016,7 @@ describe("connecting a component to store", () => {
"item:usestore", "item:usestore",
"item:render", "item:render",
"app:usestore", "app:usestore",
"app:render" "app:render",
]); ]);
expect(fixture.innerHTML).toBe('<div class="todoapp"></div>'); expect(fixture.innerHTML).toBe('<div class="todoapp"></div>');
}); });
@@ -1026,7 +1026,7 @@ describe("connecting a component to store", () => {
class App extends Component { class App extends Component {
static template = xml`<div><t t-esc="store.msg"/></div>`; static template = xml`<div><t t-esc="store.msg"/></div>`;
store = useStore(s => ({ msg: s.msg })); store = useStore((s) => ({ msg: s.msg }));
willPatch() { willPatch() {
steps.push("willpatch"); steps.push("willpatch");
@@ -1040,7 +1040,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
setMsg({ state }, c) { setMsg({ state }, c) {
state.msg = c; state.msg = c;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -1061,7 +1061,7 @@ describe("connecting a component to store", () => {
class Child extends Component { class Child extends Component {
static template = xml`<div><t t-esc="store.val"/></div>`; static template = xml`<div><t t-esc="store.val"/></div>`;
store = useStore(s => s); store = useStore((s) => s);
} }
class Parent extends Component { class Parent extends Component {
@@ -1099,7 +1099,7 @@ describe("connecting a component to store", () => {
class Child extends Component { class Child extends Component {
static template = xml`<div><t t-esc="store.val"/></div>`; static template = xml`<div><t t-esc="store.val"/></div>`;
store = useStore(s => { store = useStore((s) => {
steps.push("child selector"); steps.push("child selector");
return s; return s;
}); });
@@ -1107,7 +1107,7 @@ describe("connecting a component to store", () => {
class Parent extends Component { class Parent extends Component {
static template = xml`<div><Child t-if="store.child" /></div>`; static template = xml`<div><Child t-if="store.child" /></div>`;
static components = { Child }; static components = { Child };
store = useStore(s => { store = useStore((s) => {
steps.push("parent selector"); steps.push("parent selector");
return s; return s;
}); });
@@ -1118,7 +1118,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
toggleChild({ state }) { toggleChild({ state }) {
state.child = !state.child; state.child = !state.child;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
(<any>env).store = store; (<any>env).store = store;
@@ -1137,18 +1137,18 @@ describe("connecting a component to store", () => {
test("dispatch an action", async () => { test("dispatch an action", async () => {
class App extends Component { class App extends Component {
static template = xml`<div><t t-esc="store.counter"/></div>`; static template = xml`<div><t t-esc="store.counter"/></div>`;
store = useStore(state => state); store = useStore((state) => state);
dispatch = useDispatch(); dispatch = useDispatch();
} }
const state = { const state = {
counter: 0 counter: 0,
}; };
const actions = { const actions = {
inc({ state }) { inc({ state }) {
return ++state.counter; return ++state.counter;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -1185,21 +1185,21 @@ describe("various scenarios", () => {
await Promise.resolve(); await Promise.resolve();
delete state.attachments[100]; delete state.attachments[100];
state.messages[10].attachmentIds = []; state.messages[10].attachmentIds = [];
} },
}; };
const state = { const state = {
attachments: { attachments: {
100: { 100: {
id: 100, id: 100,
name: "text.txt" name: "text.txt",
} },
}, },
messages: { messages: {
10: { 10: {
attachmentIds: [100], attachmentIds: [100],
id: 10 id: 10,
} },
} },
}; };
const store = new Store({ actions, state }); const store = new Store({ actions, state });
@@ -1220,7 +1220,7 @@ describe("various scenarios", () => {
<Attachment t-foreach="store.attachmentIds" t-key="attachmentId" t-as="attachmentId" id="attachmentId"/> <Attachment t-foreach="store.attachmentIds" t-key="attachmentId" t-as="attachmentId" id="attachmentId"/>
</div>`; </div>`;
store = useStore(state => ({ attachmentIds: state.messages[10].attachmentIds })); store = useStore((state) => ({ attachmentIds: state.messages[10].attachmentIds }));
static components = { Attachment }; static components = { Attachment };
state = { isAttachmentDeleted: false }; state = { isAttachmentDeleted: false };
dispatch = useDispatch(); dispatch = useDispatch();
+2 -2
View File
@@ -20,7 +20,7 @@ debugOwl(owl, {});
test("can log full lifecycle", async () => { test("can log full lifecycle", async () => {
const steps: string[] = []; const steps: string[] = [];
const log = console.log; const log = console.log;
console.log = arg => steps.push(arg); console.log = (arg) => steps.push(arg);
class Child extends Component { class Child extends Component {
static template = xml`<div>child</div>`; static template = xml`<div>child</div>`;
@@ -51,7 +51,7 @@ test("can log full lifecycle", async () => {
"[OWL_DEBUG] Child<id=2> rendering template", "[OWL_DEBUG] Child<id=2> rendering template",
"[OWL_DEBUG] Parent<id=1> willPatch", "[OWL_DEBUG] Parent<id=1> willPatch",
"[OWL_DEBUG] Child<id=2> mounted", "[OWL_DEBUG] Child<id=2> mounted",
"[OWL_DEBUG] Parent<id=1> patched" "[OWL_DEBUG] Parent<id=1> patched",
]); ]);
console.log = log; console.log = log;
}); });
+2 -2
View File
@@ -19,7 +19,7 @@ debugOwl(owl, { logScheduler: true });
test("can log scheduler start and stop", async () => { test("can log scheduler start and stop", async () => {
const steps: string[] = []; const steps: string[] = [];
const log = console.log; const log = console.log;
console.log = arg => steps.push(arg); console.log = (arg) => steps.push(arg);
class Child extends Component { class Child extends Component {
static template = xml`<div>child</div>`; static template = xml`<div>child</div>`;
@@ -44,7 +44,7 @@ test("can log scheduler start and stop", async () => {
"[OWL_DEBUG] Child<id=2> rendering template", "[OWL_DEBUG] Child<id=2> rendering template",
"[OWL_DEBUG] Child<id=2> mounted", "[OWL_DEBUG] Child<id=2> mounted",
"[OWL_DEBUG] Parent<id=1> mounted", "[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue" "[OWL_DEBUG] scheduler: stop running tasks queue",
]); ]);
console.log = log; console.log = log;
}); });
+2 -2
View File
@@ -19,7 +19,7 @@ debugOwl(owl, { logScheduler: true });
test("log a specific message for render method calls if component is not mounted", async () => { test("log a specific message for render method calls if component is not mounted", async () => {
const steps: string[] = []; const steps: string[] = [];
const log = console.log; const log = console.log;
console.log = arg => steps.push(arg); console.log = (arg) => steps.push(arg);
class Parent extends Component { class Parent extends Component {
static template = xml`<div><t t-esc="state.value"/></div>`; static template = xml`<div><t t-esc="state.value"/></div>`;
@@ -40,7 +40,7 @@ test("log a specific message for render method calls if component is not mounted
"[OWL_DEBUG] Parent<id=1> mounted", "[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue", "[OWL_DEBUG] scheduler: stop running tasks queue",
"[OWL_DEBUG] Parent<id=1> willUnmount", "[OWL_DEBUG] Parent<id=1> willUnmount",
"[OWL_DEBUG] Parent<id=1> render (warning: component is not mounted, this render has no effect)" "[OWL_DEBUG] Parent<id=1> render (warning: component is not mounted, this render has no effect)",
]); ]);
console.log = log; console.log = log;
}); });
+2 -2
View File
@@ -19,7 +19,7 @@ debugOwl(owl, { logScheduler: true });
test("log a sub component with non stringifiable props", async () => { test("log a sub component with non stringifiable props", async () => {
const steps: string[] = []; const steps: string[] = [];
const log = console.log; const log = console.log;
console.log = arg => steps.push(arg); console.log = (arg) => steps.push(arg);
class Child extends Component { class Child extends Component {
static template = xml`<span><t t-esc="props.obj.val"/></span>`; static template = xml`<span><t t-esc="props.obj.val"/></span>`;
@@ -51,7 +51,7 @@ test("log a sub component with non stringifiable props", async () => {
"[OWL_DEBUG] Parent<id=1> mounted", "[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue", "[OWL_DEBUG] scheduler: stop running tasks queue",
"[OWL_DEBUG] Parent<id=1> willUnmount", "[OWL_DEBUG] Parent<id=1> willUnmount",
"[OWL_DEBUG] Child<id=2> willUnmount" "[OWL_DEBUG] Child<id=2> willUnmount",
]); ]);
console.log = log; console.log = log;
}); });
+7 -7
View File
@@ -60,14 +60,14 @@ function getFiles(path: string[] = []): FileData[] {
if (path.length === 0) { if (path.length === 0) {
const baseFiles: FileData[] = [ const baseFiles: FileData[] = [
{ name: "README.md", path: [], links: [], sections: [], fullName: "README.md" }, { name: "README.md", path: [], links: [], sections: [], fullName: "README.md" },
{ name: "roadmap.md", path: [], links: [], sections: [], fullName: "roadmap.md" } { name: "roadmap.md", path: [], links: [], sections: [], fullName: "roadmap.md" },
]; ];
const rest = getFiles(["doc"]); const rest = getFiles(["doc"]);
const result = baseFiles.concat(rest); const result = baseFiles.concat(rest);
result.forEach(addMardownData); result.forEach(addMardownData);
return result; return result;
} }
const files = fs.readdirSync(path.join("/"), { withFileTypes: true }).map(f => { const files = fs.readdirSync(path.join("/"), { withFileTypes: true }).map((f) => {
if (f.isDirectory()) { if (f.isDirectory()) {
return getFiles(path.concat(f.name)); return getFiles(path.concat(f.name));
} }
@@ -78,8 +78,8 @@ function getFiles(path: string[] = []): FileData[] {
path, path,
links: [], links: [],
sections: [], sections: [],
fullName fullName,
} },
]; ];
}); });
return Array.prototype.concat(...files); return Array.prototype.concat(...files);
@@ -127,7 +127,7 @@ export function isLinkValid(link: MarkDownLink, current: FileData, files: FileDa
} }
// Step 4: check if there is a matching file // Step 4: check if there is a matching file
let target: FileData | undefined = files.find(f => f.fullName === linkFullName); let target: FileData | undefined = files.find((f) => f.fullName === linkFullName);
if (!target) { if (!target) {
return false; return false;
} }
@@ -135,7 +135,7 @@ export function isLinkValid(link: MarkDownLink, current: FileData, files: FileDa
// Step 5: if necessary, check if there is a corresponding link inside the target // Step 5: if necessary, check if there is a corresponding link inside the target
// link name // link name
if (hash) { if (hash) {
if (!target.sections.find(s => s.slug === hash)) { if (!target.sections.find((s) => s.slug === hash)) {
return false; return false;
} }
} }
@@ -153,7 +153,7 @@ function slugify(str) {
.toLowerCase() .toLowerCase()
.replace(/\//g, "") // remove / .replace(/\//g, "") // remove /
.replace(/\s+/g, "-") // Replace spaces with - .replace(/\s+/g, "-") // Replace spaces with -
.replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters .replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
.replace(/&/g, "-and-") // Replace & with and .replace(/&/g, "-and-") // Replace & with and
.replace(/[^\w\-]+/g, "") // Remove all non-word characters .replace(/[^\w\-]+/g, "") // Remove all non-word characters
.replace(/\-\-+/g, "-") // Replace multiple - with single - .replace(/\-\-+/g, "-") // Replace multiple - with single -
+199 -192
View File
File diff suppressed because it is too large Load Diff
+255 -284
View File
@@ -1,4 +1,4 @@
import { SAMPLES } from "./samples.js"; import { SAMPLES as samples } from "./samples.js" ;
const { useState, useRef, onMounted, onWillUnmount } = owl.hooks; const { useState, useRef, onMounted, onWillUnmount } = owl.hooks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Constants, helpers, utils // Constants, helpers, utils
@@ -6,18 +6,18 @@ const { useState, useRef, onMounted, onWillUnmount } = owl.hooks;
let owlJS; let owlJS;
async function owlSourceCode() { async function owlSourceCode() {
if (owlJS) { if (owlJS) {
return owlJS;
}
const result = await fetch("../owl.js");
owlJS = await result.text();
return owlJS; return owlJS;
}
const result = await fetch("../owl.js");
owlJS = await result.text();
return owlJS;
} }
const MODES = { const MODES = {
js: "ace/mode/javascript", js: "ace/mode/javascript",
css: "ace/mode/css", css: "ace/mode/css",
xml: "ace/mode/xml" xml: "ace/mode/xml"
}; };
const DEFAULT_XML = `<templates> const DEFAULT_XML = `<templates>
@@ -71,22 +71,23 @@ if __name__ == "__main__":
* Make an iframe, with all the js, css and xml properly injected. * Make an iframe, with all the js, css and xml properly injected.
*/ */
function makeCodeIframe(js, css, xml) { function makeCodeIframe(js, css, xml) {
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, ""); const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "");
// create iframe // create iframe
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
iframe.className += "h-full w-full";
iframe.onload = () => { iframe.onload = () => {
const doc = iframe.contentDocument; const doc = iframe.contentDocument;
// inject js // inject js
const owlScript = doc.createElement("script"); const owlScript = doc.createElement("script");
owlScript.type = "text/javascript"; owlScript.type = "text/javascript";
owlScript.src = "../owl.js"; owlScript.src = "../owl.js";
owlScript.addEventListener("load", () => { owlScript.addEventListener("load", () => {
const script = doc.createElement("script"); const script = doc.createElement("script");
script.type = "text/javascript"; script.type = "text/javascript";
const content = ` const content = `
{ {
owl.config.mode = 'dev'; owl.config.mode = 'dev';
let templates = \`${sanitizedXML}\`; let templates = \`${sanitizedXML}\`;
@@ -94,32 +95,32 @@ function makeCodeIframe(js, css, xml) {
owl.Component.env = { qweb }; owl.Component.env = { qweb };
} }
${js}`; ${js}`;
script.innerHTML = content; script.innerHTML = content;
doc.body.appendChild(script); doc.body.appendChild(script);
}); });
doc.head.appendChild(owlScript); doc.head.appendChild(owlScript);
// inject css // inject css
const style = document.createElement("style"); const style = document.createElement("style");
style.innerHTML = css; style.innerHTML = css;
doc.head.appendChild(style); doc.head.appendChild(style);
}; };
return iframe; return iframe;
} }
/** /**
* Make a zip file containing a functioning application * Make a zip file containing a functioning application
*/ */
async function makeApp(js, css, xml) { async function makeApp(js, css, xml) {
await owl.utils.loadJS("libs/jszip.min.js"); await owl.utils.loadJS("libs/jszip.min.js");
const zip = new JSZip(); const zip = new JSZip();
const processedJS = js const processedJS = js
.split("\n") .split("\n")
.map(l => (l === "" ? "" : " " + l)) .map(l => (l === "" ? "" : " " + l))
.join("\n"); .join("\n");
const JS = ` const JS = `
/** /**
* This is the javascript code defined in the playground. * This is the javascript code defined in the playground.
* In a larger application, this code should probably be moved in different * In a larger application, this code should probably be moved in different
@@ -150,278 +151,248 @@ async function start() {
start(); start();
`; `;
zip.file("app.js", JS); zip.file("app.js", JS);
zip.file("app.css", css); zip.file("app.css", css);
zip.file("app.py", APP_PY); zip.file("app.py", APP_PY);
zip.file("app.xml", xml); zip.file("app.xml", xml);
zip.file("index.html", DEFAULT_HTML); zip.file("index.html", DEFAULT_HTML);
zip.file("owl.js", owlSourceCode()); zip.file("owl.js", owlSourceCode());
return zip.generateAsync({ type: "blob" }); return zip.generateAsync({ type: "blob" });
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// SAMPLES // SAMPLES
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
function loadSamples() { function loadSamples() {
let result = SAMPLES.slice(); let result = samples.slice();
const localSample = localStorage.getItem("owl-playground-local-sample"); const localSample = localStorage.getItem("owl-playground-local-sample");
if (localSample) { if (localSample) {
const { js, css, xml } = JSON.parse(localSample); const { js, css, xml } = JSON.parse(localSample);
result.unshift({ result.unshift({
description: "Local Storage Code", description: "Local Storage Code",
code: js, code: js,
xml, xml,
css css
}); });
} }
return result; return result;
} }
function saveLocalSample(js, css, xml) { function saveLocalSample(js, css, xml) {
const str = JSON.stringify({ js, css, xml }); const str = JSON.stringify({ js, css, xml });
localStorage.setItem("owl-playground-local-sample", str); localStorage.setItem("owl-playground-local-sample", str);
} }
function deleteLocalSample() { function deleteLocalSample() {
localStorage.removeItem("owl-playground-local-sample"); localStorage.removeItem("owl-playground-local-sample");
} }
function useSamples() { function useSamples() {
const samples = loadSamples(); return loadSamples();
const component = owl.Component.current;
let interval;
onMounted(() => {
const state = component.state;
interval = setInterval(() => {
if (component.isDirty) {
saveLocalSample(state.js, state.css, state.xml);
}
}, 1000);
});
onWillUnmount(() => {
clearInterval(interval);
});
return samples;
}
//------------------------------------------------------------------------------
// Tabbed editor
//------------------------------------------------------------------------------
class TabbedEditor extends owl.Component {
constructor(parent, props) {
super(parent, props);
this.state = useState({
currentTab: props.js !== false ? "js" : props.xml ? "xml" : "css"
});
this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.sessions = {};
this._setupSessions(props);
this.editorNode = useRef("editor");
this._updateCode = this._updateCode.bind(this);
}
mounted() {
this.editor = this.editor || ace.edit(this.editorNode.el);
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", this._updateCode);
this.interval = setInterval(this._updateCode, 3000);
}
willUnmount() {
clearInterval(this.interval);
this.editor.off("blur", this._updateCode);
}
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 !== false ? "js" : this.props.xml ? "xml" : "css";
content = this.props[tab];
this.state.currentTab = tab;
}
if (this.editor.getValue() !== content) {
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] !== false && !this.sessions[tab]) {
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
this.sessions[tab].setOption("useWorker", false);
const tabSize = tab === "xml" ? 2 : 4;
this.sessions[tab].setOption("tabSize", tabSize);
this.sessions[tab].setUndoManager(new ace.UndoManager());
}
}
}
_updateCode() {
const editorValue = this.editor.getValue();
const propsValue = this.props[this.state.currentTab];
if (editorValue !== propsValue) {
this.trigger("updateCode", {
type: this.state.currentTab,
value: editorValue
});
}
}
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// MAIN APP // MAIN APP
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class App extends owl.Component {
constructor(...args) {
super(...args);
this.version = owl.__info__.version;
this.SAMPLES = useSamples();
this.isDirty = false;
this.state = useState({ class Tab extends owl.Component {
js: this.SAMPLES[0].code,
css: this.SAMPLES[0].css || "",
xml: this.SAMPLES[0].xml || DEFAULT_XML,
displayWelcome: true,
splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null
});
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
this.runCode = owl.utils.debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
this.content = useRef("content");
}
runCode() {
this.content.el.innerHTML = "";
this.state.displayWelcome = false;
const { js, css, xml } = this.state;
const subiframe = makeCodeIframe(js, css, xml);
this.content.el.appendChild(subiframe);
}
setSample(ev) {
const sample = this.SAMPLES.find(s => s.description === ev.target.value);
this.state.js = sample.code;
this.state.css = sample.css || "";
this.state.xml = sample.xml || DEFAULT_XML;
deleteLocalSample();
this.isDirty = false;
}
get leftPaneStyle() {
return `width:${this.state.leftPaneWidth}px`;
}
get rightPaneStyle() {
return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
}
get topEditorStyle() {
return `flex: 0 0 ${this.state.topPanelHeight}px`;
}
onMouseDown() {
const resizer = ev => {
this.state.leftPaneWidth = ev.clientX;
};
document.body.addEventListener("mousemove", resizer);
for (let iframe of document.getElementsByTagName("iframe")) {
iframe.classList.add("disabled");
}
document.body.addEventListener("mouseup", () => {
document.body.removeEventListener("mousemove", resizer);
for (let iframe of document.getElementsByTagName("iframe")) {
iframe.classList.remove("disabled");
}
});
}
updateCode(ev) {
if (this.state[ev.detail.type] !== ev.detail.value) {
this.state[ev.detail.type] = ev.detail.value;
this.isDirty = true;
}
}
toggleLayout() {
this.state.splitLayout = !this.state.splitLayout;
}
updatePanelHeight(ev) {
if (!ev.detail.delta) {
return;
}
let height = this.state.topPanelHeight;
if (!height) {
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
}
this.state.topPanelHeight = height + ev.detail.delta;
}
async downloadCode() {
const { js, css, xml } = this.state;
const content = await makeApp(js, css, xml);
await owl.utils.loadJS("libs/FileSaver.min.js");
saveAs(content, "app.zip");
}
} }
App.components = { TabbedEditor };
class Editor extends owl.Component {
static props = ['js', 'xml', 'css']
state = useState({
currentExtension: 'js',
js: this.props.js,
css: this.props.css,
xml: this.props.xml,
})
jsModel = null;
cssModel = null;
xmlModel = null;
editor = null;
willUpdateProps(nextProps) {
console.log(nextProps);
this.state.js = nextProps.js;
this.state.css = nextProps.css;
this.state.xml = nextProps.xml;
this.jsModel = null;
this.cssModel = null;
this.xmlModel = null;
this._openFile(this.state.currentExtension, false)
}
_run(ev) {
this._saveCodeState();
this.trigger('run-project', {
js: this.state.js,
xml: this.state.xml,
css: this.state.css
})
}
_openFile(extension, save = true) {
if (save) this._saveCodeState();
this.state.currentExtension = extension;
let language = null;
let model = null;
if (extension === 'js') {
console.log("here")
if (this.jsModel) {
model = this.jsModel;
}
else {
language = 'javascript'
}
}
if (extension === 'xml') {
if (this.xmlModel) {
model = this.xmlModel;
}
else {
language = 'xml'
}
}
if (extension === 'css') {
if (this.cssModel) {
model = this.cssModel;
}
else {
language = 'css'
}
}
console.log(model)
if (!model) {
model = monaco.editor.createModel(
this.state[this.state.currentExtension],
language,
null
)
if (extension === 'js') {
this.jsModel = model;
}
if (extension === 'xml') {
this.xmlModel = model;
}
if (extension === 'css') {
this.cssModel = model;
}
}
this.editor.setModel(model)
}
mounted() {
this.editor = monaco.editor.create(document.getElementById('container'), {
value: this.state[this.state.currentExtension],
language: 'javascript',
automaticLayout: true,
minimap: {
enabled: false
}
});
}
_saveCodeState() {
this.state[this.state.currentExtension] = this.editor.getValue();
}
_openCSS(ev) {
this._openFile('css')
}
_openXML(ev) {
this._openFile('xml')
}
_openJS(ev) {
this._openFile('js')
}
}
Editor.components = { Tab }
class App extends owl.Component {
samples = [];
constructor(...args) {
super(...args);
this.version = owl.__info__.version;
this.samples = useSamples();
this.state = useState({
js: this.samples[0].code,
css: this.samples[0].css || "",
xml: this.samples[0].xml || DEFAULT_XML,
});
this.runCode = owl.utils.debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
this.content = useRef("iframe-container");
}
_run(ev) {
this.content.el.innerHTML = "";
const { js, css, xml } = ev.detail;
const subiframe = makeCodeIframe(js, css, xml);
this.content.el.appendChild(subiframe);
}
_setSample(ev) {
const sample = this.samples.find(s => s.description === ev.target.value);
this.state.js = sample.code;
this.state.css = sample.css || "";
this.state.xml = sample.xml || DEFAULT_XML;
deleteLocalSample();
}
}
App.components = { Editor }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Application initialization // Application initialization
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
async function start() { async function start() {
document.title = `${document.title} (v${owl.__info__.version})`; document.title = `${document.title} (v${owl.__info__.version})`;
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`; const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
console.info(`This application is using Owl built with the following commit:`, commit); console.info(`This application is using Owl built with the following commit:`, commit);
const [templates] = await Promise.all([ const [templates] = await Promise.all([
owl.utils.loadFile("templates.xml"), owl.utils.loadFile("templates.xml"),
owl.utils.whenReady() owl.utils.whenReady()
]); ]);
const qweb = new owl.QWeb({ templates }); const qweb = new owl.QWeb({ templates });
owl.Component.env = { qweb }; owl.Component.env = { qweb };
const app = new App(); const app = new App();
app.mount(document.body); app.mount(document.body);
} }
start(); start();
+21 -15
View File
@@ -1,20 +1,26 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html>
<head> <head>
<meta charset="utf-8"> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title>OWL Playground</title> <link
<link rel="icon" href="data:,"> rel="stylesheet"
data-name="vs/editor/editor.main"
<script src="libs/ace.js" type="text/javascript" charset="utf-8"></script> href="../../node_modules/monaco-editor/min/vs/editor/editor.main.css"
/>
<script src="../owl.js"></script> <script src="../owl.js"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/solid.css" integrity="sha384-QokYePQSOwpBDuhlHOsX0ymF6R/vLk/UQVz3WHa6wygxI5oGTmDTv8wahFOSspdm" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/fontawesome.css" integrity="sha384-vd1e11sR28tEK9YANUtpIOdjGW14pS87bUBuOIoBILVWLFnS+MCX9T6MMf0VdPGq" crossorigin="anonymous">
<!-- Application JS/CSS -->
<link rel="stylesheet" href="playground.css">
<script type="module" src="app.js"></script> <script type="module" src="app.js"></script>
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="h-screen bg-gray-200">
</head> <script>
<body> var require = { paths: { vs: '../../node_modules/monaco-editor/min/vs' } };
</body> </script>
<script src="../../node_modules/monaco-editor/min/vs/loader.js"></script>
<script src="../../node_modules/monaco-editor/min/vs/editor/editor.main.nls.js"></script>
<script src="../../node_modules/monaco-editor/min/vs/editor/editor.main.js"></script>
</body>
</html> </html>
+427
View File
@@ -0,0 +1,427 @@
import { SAMPLES } from "./samples.js";
const { useState, useRef, onMounted, onWillUnmount } = owl.hooks;
//------------------------------------------------------------------------------
// Constants, helpers, utils
//------------------------------------------------------------------------------
let owlJS;
async function owlSourceCode() {
if (owlJS) {
return owlJS;
}
const result = await fetch("../owl.js");
owlJS = await result.text();
return owlJS;
}
const MODES = {
js: "ace/mode/javascript",
css: "ace/mode/css",
xml: "ace/mode/xml"
};
const DEFAULT_XML = `<templates>
</templates>`;
const DEFAULT_HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL App</title>
<link rel="icon" href="data:,">
<script src="owl.js"></script>
<link rel="stylesheet" href="app.css">
<script src="app.js"></script>
</head>
<body>
</body>
</html>
`;
const APP_PY = `#!/usr/bin/env python3
import threading
import time
from http.server import SimpleHTTPRequestHandler, HTTPServer
def start_server():
SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
httpd.serve_forever()
url = 'http://127.0.0.1:3600'
if __name__ == "__main__":
print("Owl Application")
print("---------------")
print("Server running on: {}".format(url))
threading.Thread(target=start_server, daemon=True).start()
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
httpd.server_close()
quit(0)
`;
/**
* Make an iframe, with all the js, css and xml properly injected.
*/
function makeCodeIframe(js, css, xml) {
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "");
// create iframe
const iframe = document.createElement("iframe");
iframe.onload = () => {
const doc = iframe.contentDocument;
// inject js
const owlScript = doc.createElement("script");
owlScript.type = "text/javascript";
owlScript.src = "../owl.js";
owlScript.addEventListener("load", () => {
const script = doc.createElement("script");
script.type = "text/javascript";
const content = `
{
owl.config.mode = 'dev';
let templates = \`${sanitizedXML}\`;
const qweb = new owl.QWeb({ templates });
owl.Component.env = { qweb };
}
${js}`;
script.innerHTML = content;
doc.body.appendChild(script);
});
doc.head.appendChild(owlScript);
// inject css
const style = document.createElement("style");
style.innerHTML = css;
doc.head.appendChild(style);
};
return iframe;
}
/**
* Make a zip file containing a functioning application
*/
async function makeApp(js, css, xml) {
await owl.utils.loadJS("libs/jszip.min.js");
const zip = new JSZip();
const processedJS = js
.split("\n")
.map(l => (l === "" ? "" : " " + l))
.join("\n");
const JS = `
/**
* This is the javascript code defined in the playground.
* In a larger application, this code should probably be moved in different
* sub files.
*/
function app() {
${processedJS}
}
/**
* Initialization code
* This code load templates, and make sure everything is properly connected.
*/
async function start() {
let templates;
try {
templates = await owl.utils.loadFile('app.xml');
} catch(e) {
console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
return;
}
const env = { qweb: new owl.QWeb({templates})};
owl.Component.env = env;
await owl.utils.whenReady();
app();
}
start();
`;
zip.file("app.js", JS);
zip.file("app.css", css);
zip.file("app.py", APP_PY);
zip.file("app.xml", xml);
zip.file("index.html", DEFAULT_HTML);
zip.file("owl.js", owlSourceCode());
return zip.generateAsync({ type: "blob" });
}
//------------------------------------------------------------------------------
// SAMPLES
//------------------------------------------------------------------------------
function loadSamples() {
let result = SAMPLES.slice();
const localSample = localStorage.getItem("owl-playground-local-sample");
if (localSample) {
const { js, css, xml } = JSON.parse(localSample);
result.unshift({
description: "Local Storage Code",
code: js,
xml,
css
});
}
return result;
}
function saveLocalSample(js, css, xml) {
const str = JSON.stringify({ js, css, xml });
localStorage.setItem("owl-playground-local-sample", str);
}
function deleteLocalSample() {
localStorage.removeItem("owl-playground-local-sample");
}
function useSamples() {
const samples = loadSamples();
const component = owl.Component.current;
let interval;
onMounted(() => {
const state = component.state;
interval = setInterval(() => {
if (component.isDirty) {
saveLocalSample(state.js, state.css, state.xml);
}
}, 1000);
});
onWillUnmount(() => {
clearInterval(interval);
});
return samples;
}
//------------------------------------------------------------------------------
// Tabbed editor
//------------------------------------------------------------------------------
class TabbedEditor extends owl.Component {
constructor(parent, props) {
super(parent, props);
this.state = useState({
currentTab: props.js !== false ? "js" : props.xml ? "xml" : "css"
});
this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.sessions = {};
this._setupSessions(props);
this.editorNode = useRef("editor");
this._updateCode = this._updateCode.bind(this);
}
mounted() {
this.editor = this.editor || ace.edit(this.editorNode.el);
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", this._updateCode);
this.interval = setInterval(this._updateCode, 3000);
}
willUnmount() {
clearInterval(this.interval);
this.editor.off("blur", this._updateCode);
}
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 !== false ? "js" : this.props.xml ? "xml" : "css";
content = this.props[tab];
this.state.currentTab = tab;
}
if (this.editor.getValue() !== content) {
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] !== false && !this.sessions[tab]) {
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
this.sessions[tab].setOption("useWorker", false);
const tabSize = tab === "xml" ? 2 : 4;
this.sessions[tab].setOption("tabSize", tabSize);
this.sessions[tab].setUndoManager(new ace.UndoManager());
}
}
}
_updateCode() {
const editorValue = this.editor.getValue();
const propsValue = this.props[this.state.currentTab];
if (editorValue !== propsValue) {
this.trigger("updateCode", {
type: this.state.currentTab,
value: editorValue
});
}
}
}
//------------------------------------------------------------------------------
// MAIN APP
//------------------------------------------------------------------------------
class App extends owl.Component {
constructor(...args) {
super(...args);
this.version = owl.__info__.version;
this.SAMPLES = useSamples();
this.isDirty = false;
this.state = useState({
js: this.SAMPLES[0].code,
css: this.SAMPLES[0].css || "",
xml: this.SAMPLES[0].xml || DEFAULT_XML,
displayWelcome: true,
splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null
});
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
this.runCode = owl.utils.debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
this.content = useRef("content");
}
runCode() {
this.content.el.innerHTML = "";
this.state.displayWelcome = false;
const { js, css, xml } = this.state;
const subiframe = makeCodeIframe(js, css, xml);
this.content.el.appendChild(subiframe);
}
setSample(ev) {
const sample = this.SAMPLES.find(s => s.description === ev.target.value);
this.state.js = sample.code;
this.state.css = sample.css || "";
this.state.xml = sample.xml || DEFAULT_XML;
deleteLocalSample();
this.isDirty = false;
}
get leftPaneStyle() {
return `width:${this.state.leftPaneWidth}px`;
}
get rightPaneStyle() {
return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
}
get topEditorStyle() {
return `flex: 0 0 ${this.state.topPanelHeight}px`;
}
onMouseDown() {
const resizer = ev => {
this.state.leftPaneWidth = ev.clientX;
};
document.body.addEventListener("mousemove", resizer);
for (let iframe of document.getElementsByTagName("iframe")) {
iframe.classList.add("disabled");
}
document.body.addEventListener("mouseup", () => {
document.body.removeEventListener("mousemove", resizer);
for (let iframe of document.getElementsByTagName("iframe")) {
iframe.classList.remove("disabled");
}
});
}
updateCode(ev) {
if (this.state[ev.detail.type] !== ev.detail.value) {
this.state[ev.detail.type] = ev.detail.value;
this.isDirty = true;
}
}
toggleLayout() {
this.state.splitLayout = !this.state.splitLayout;
}
updatePanelHeight(ev) {
if (!ev.detail.delta) {
return;
}
let height = this.state.topPanelHeight;
if (!height) {
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
}
this.state.topPanelHeight = height + ev.detail.delta;
}
async downloadCode() {
const { js, css, xml } = this.state;
const content = await makeApp(js, css, xml);
await owl.utils.loadJS("libs/FileSaver.min.js");
saveAs(content, "app.zip");
}
}
App.components = { TabbedEditor };
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
document.title = `${document.title} (v${owl.__info__.version})`;
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
console.info(`This application is using Owl built with the following commit:`, commit);
const [templates] = await Promise.all([
owl.utils.loadFile("templates.xml"),
owl.utils.whenReady()
]);
const qweb = new owl.QWeb({ templates });
owl.Component.env = { qweb };
const app = new App();
app.mount(document.body);
}
start();
+21
View File
@@ -0,0 +1,21 @@
<!---->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL Playground</title>
<link rel="icon" href="data:,">
<script src="libs/ace.js" type="text/javascript" charset="utf-8"></script>
<script src="../owl.js"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/solid.css" integrity="sha384-QokYePQSOwpBDuhlHOsX0ymF6R/vLk/UQVz3WHa6wygxI5oGTmDTv8wahFOSspdm" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/fontawesome.css" integrity="sha384-vd1e11sR28tEK9YANUtpIOdjGW14pS87bUBuOIoBILVWLFnS+MCX9T6MMf0VdPGq" crossorigin="anonymous">
<!-- Application JS/CSS -->
<link rel="stylesheet" href="playground.css">
<script type="module" src="app.js"></script>
</head>
<body>
</body>
</html>
+57
View File
@@ -0,0 +1,57 @@
<templates>
<div t-name="TabbedEditor" class="tabbed-editor">
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
<t t-foreach="['js', 'xml', 'css']" t-as="tab">
<a t-ref="{{tab}}" t-if="props[tab] !== false" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="setTab(tab)">
<t t-esc="tab"/>
</a>
</t>
</div>
<div class="code-editor" t-ref="editor"></div>
</div>
<div t-name="App" class="playground">
<div class="left-bar" t-att-class="{split: state.splitLayout}"
t-att-style="leftPaneStyle"
t-on-updateCode="updateCode">
<div class="menubar">
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a>
<select t-on-change="setSample">
<option t-foreach="SAMPLES" t-as="sample" t-key="sample_index">
<t t-esc="sample.description"/>
</option>
</select>
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
</div>
<TabbedEditor
js="state.js"
css="!state.splitLayout and state.css"
xml="!state.splitLayout and state.xml"
t-att-style="topEditorStyle"/>
<t t-if="state.splitLayout">
<div class="separator horizontal"/>
<TabbedEditor
js="false"
css="state.css"
xml="state.xml"
resizeable="true"
t-on-updatePanelHeight="updatePanelHeight"/>
</t>
</div>
<div class="separator vertical" t-on-mousedown="onMouseDown"/>
<div class="right-pane" t-att-style="rightPaneStyle">
<div class="welcome" t-if="state.displayWelcome">
<div>🦉 Odoo Web Library 🦉</div>
<div>v<t t-esc="version"/></div>
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
<div class="note">
<p>Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step!
For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version.
</p>
</div>
</div>
<div class="content" t-ref="content"/>
</div>
</div>
</templates>
+74 -52
View File
@@ -1,57 +1,79 @@
<templates> <templates>
<div t-name="TabbedEditor" class="tabbed-editor">
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
<t t-foreach="['js', 'xml', 'css']" t-as="tab">
<a t-ref="{{tab}}" t-if="props[tab] !== false" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="setTab(tab)">
<t t-esc="tab"/>
</a>
</t>
</div>
<div class="code-editor" t-ref="editor"></div>
</div>
<div t-name="App" class="playground"> <div t-name="App" t-on-run-project="_run" class="h-full flex">
<div class="left-bar" t-att-class="{split: state.splitLayout}"
t-att-style="leftPaneStyle" <Editor js="state.js" xml="state.xml" css="state.css" />
t-on-updateCode="updateCode">
<div class="menubar">
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a> <div class="flex flex-col w-1/2 h-full">
<select t-on-change="setSample">
<option t-foreach="SAMPLES" t-as="sample" t-key="sample_index"> <div class="bg-purple-300 mx-2 my-1 p-2 rounded-sm" style="background-color: #865A7B">
<t t-esc="sample.description"/> <div class="flex items-center">
</option> <div class="relative">
</select> <select t-on-change="_setSample" class="block appearance-none w-full bg-gray-200 border border-gray-200 text-gray-700 py-1 px-2 pr-8 rounded-sm leading-tight focus:outline-none focus:bg-white focus:border-gray-500">
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a> <t t-foreach="samples" t-as="sample" t-key="sample_index">
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a> <option>
<t t-esc="sample.description"/>
</option>
</t>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
<svg class="fill-current h-4 w-4"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"></path>
</svg>
</div>
</div>
</div>
</div>
<div t-ref="iframe-container" class="bg-white flex-grow m-2 rounded-sm shadow"></div>
</div> </div>
<TabbedEditor
js="state.js"
css="!state.splitLayout and state.css" </div>
xml="!state.splitLayout and state.xml"
t-att-style="topEditorStyle"/> <t t-name="Editor" t-on-open-file="_openFile">
<t t-if="state.splitLayout"> <div class="resize-x w-1/2">
<div class="separator horizontal"/>
<TabbedEditor <div class="flex justify-between items-center" style="background-color: #865A7B">
js="false" <ul class="flex justify-start">
css="state.css" <li>
xml="state.xml" <Tab t-on-click="_openXML">
resizeable="true" xml
t-on-updatePanelHeight="updatePanelHeight"/> </Tab>
</t> </li>
</div> <li>
<div class="separator vertical" t-on-mousedown="onMouseDown"/> <Tab t-on-click="_openJS">
<div class="right-pane" t-att-style="rightPaneStyle"> js
<div class="welcome" t-if="state.displayWelcome"> </Tab>
<div>🦉 Odoo Web Library 🦉</div> </li>
<div>v<t t-esc="version"/></div> <li>
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div> <Tab t-on-click="_openCSS">
<div class="note"> css
<p>Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step! </Tab>
For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version. </li>
</p> </ul>
</div> <ul class="flex justify-end m-2">
<li>
<button class="h-6 w-6 bg-white rounded-full p-1 shadow" t-on-click="_run">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</button>
</li>
</ul>
</div>
<div id="container" class="h-full"></div>
</div> </div>
<div class="content" t-ref="content"/> </t>
</div>
</div> <t t-name="Tab">
<button class="bg-white hover:bg-gray-300 uppercase rounded-sm px-2 shadow mx-1">
<t t-slot="default"/>
</button>
</t>
</templates> </templates>