Compare commits

...

24 Commits

Author SHA1 Message Date
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
Géry Debongnie 211f6ebdd6 [REL] v1.0.6 2020-04-17 15:16:24 +02:00
Géry Debongnie 1707bd240d [IMP] add browser bindings to standard environment
This could be done by each application, but it does cost only a few
lines of code, and it helps standardizing the Owl ecosystem.

For example, some library (such as o_spreadsheet) needs to mock side
effects, and Odoo also needs to do that, so this prevents duplicated effort.

closes #686
2020-04-17 13:39:46 +02:00
Géry Debongnie 142b69823f [IMP] types: do not make Env an indexed type
Before this commit, Env was an indexed type, this means that one could
write env.anything, and it would accept it as a valid type. This is
actually quite dangerous, because we lose the typing advantages for all
keys that are properly defined.

For example, if a component is defined as:

class MyComponent extends Component<Props> {
 ...
}

Then Typescript will let it use anything from the environment, even if
it is wrong.  So, most properly typed Typescript applications should use
instead a sub environment:

interface MyAppEnv extends Env {
  someKey: someValue
}

Then, the component should be defined this way:

class MyComponent extends Component<Props, MyAppEnv> {
 ...
}

Before this commit, any typos in the environment accesses would not be
noticed by typescript.
2020-04-17 13:39:46 +02:00
Joseph Caburnay 94c8bce810 [IMP] allow anonymous Component extensions
It is possible that a Component is extended dynamically and if this is
the case, the class that extends it can be anonymous, with property
name=''. If this is the case, current implementation interprets the empty
string to be false so the while loop is terminated without further
scanning the super classes.

In this proposal, we allow anonymous class to be scanned until its
Component ancestor. Basically, the anonymous class assumes the name of
it super.
2020-04-15 08:54:02 +02:00
Géry Debongnie ae172d42e7 [IMP] slots: add new t-set-slot directive
This new t-set-slot directive is meant to replace t-set when we need to
define the content of a sub slot. All new code should use that
directive.

The old t-set directive is still supported for now, but this should be
removed when we publish Owl 2.0.
2020-04-06 15:42:32 +02:00
Géry Debongnie ddf30a8a97 [CLEANUP] run prettier on the codebase 2020-04-06 15:42:32 +02:00
Géry Debongnie 8d0d8538ad [FIX] slots: better heuristic to determine named slot content
Unfortunately, we chose to use the directive `t-set` to define sub slot
contents in a template.

The goal was to reuse a directive for a similar use case: defining sub
template is almost the same as defining a slot content.

Obviously, this introduces a name conflict: the inner content of a
component cannot use t-set t-value anymore (nor t-set with a body
value), since they are interpreted as slot names.

We mitigate the issue here by only interpreting as slot content the
`t-set` statement located immediately below the parent component tag
name and with a body content.

However, a real fix need to introduce an additional directive to resolve
the ambiguity.
2020-04-06 15:42:32 +02:00
Joseph Caburnay 2b0315c03f [FIX] hooks: order of brackets for multiple async hooks 2020-03-30 08:46:16 +02:00
lucas c469cac315 [FIX] qweb: protect scope in t-call writing recursively
Have something like
```xml
 <div t-name="Parent">
   <t t-call="nodeTemplate">
     <t t-set="recursive_idx" t-value="1"/>
     <t t-set="node" t-value="root"/>
   </t>
 </div>

 <div t-name="nodeTemplate">
   <t t-set="recursive_idx" t-value="recursive_idx + 1"/>
   <p><t t-esc="node.val"/> <t t-esc="recursive_idx"/></p>
   <t t-foreach="node.children or []" t-as="subtree">
     <t t-call="nodeTemplate">
       <t t-set="node" t-value="subtree"/>
     </t>
   </t>
 </div>
```

Where we want to propagate a recursion index through recursive t-calls

Before this commit, it did not work as we protected the scope in order
to not leak, in the wronf manner. Namely the protected scope only took
firt level prototype properties of the original scope.

After this commit, this case works as we mark the scope as read only

solves #672
2020-03-12 13:15:28 +01:00
Lucas Perais (lpe) b4ad14edc0 [FIX] component: self mounting position keeps the reference
Have

```xml
<body t-name="webclient" />
```
and
```js

const comp = new WebClient();
comp.mount(document.body, {position: 'self'});
```

Before this commit, the body that was there before anything had happened
was *replaced* by the new body node created by the WebClient OWL component

After this commit, we ensure that the element body is the same at reference level
2020-03-05 16:35:33 +01:00
Aaron Bohy 718e5264ae [FIX] test: missing await 2020-03-03 10:42:22 +01:00
Aaron Bohy f5d019bb69 [FIX] component: concurrency issue with cancelled fiber
Since ee956a197, the rendering is skipped if the currentFiber is
completed. Unfortunately, cancelled fibers remain set in __owl__,
so when a fiber is cancelled, subsequent calls to render are
skipped.

It would be nice to reset __owl__.currentFiber to null when the
fiber is cancelled, but when trying to do so, a lot of tests fail.

Part of issue #622
Closes #665
2020-03-02 11:35:47 +01:00
Géry Debongnie afa36f52a0 [DOC] doc: fix t-debug example
also, remove a useless console.log

closes #661
2020-02-24 11:05:12 +01:00
73 changed files with 1443 additions and 852 deletions
+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.5.js](https://github.com/odoo/owl/releases/download/v1.0.5/owl.js) - [owl-1.0.9.js](https://github.com/odoo/owl/releases/download/v1.0.9/owl.js)
- [owl-1.0.5.min.js](https://github.com/odoo/owl/releases/download/v1.0.5/owl.min.js) - [owl-1.0.9.min.js](https://github.com/odoo/owl/releases/download/v1.0.9/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" }] },
] ],
}; };
} }
``` ```
+27 -3
View File
@@ -6,6 +6,7 @@
- [Setting an Environment](#setting-an-environment) - [Setting an Environment](#setting-an-environment)
- [Using a sub environment](#using-a-sub-environment) - [Using a sub environment](#using-a-sub-environment)
- [Content of an Environment](#content-of-an-environment) - [Content of an Environment](#content-of-an-environment)
- [Special keys](#special-keys)
## Overview ## Overview
@@ -93,7 +94,7 @@ Some good use cases for additional keys in the environment are:
- some configuration keys, - some configuration keys,
- session information, - session information,
- generic services (such as doing rpcs, or accessing local storage). - generic services (such as doing rpcs).
Doing it this way means that components are easily testable: we can simply Doing it this way means that components are easily testable: we can simply
create a test environment with mock services. create a test environment with mock services.
@@ -112,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,
}; };
} }
@@ -125,3 +126,26 @@ async function start() {
await app.mount(document.body); await app.mount(document.body);
} }
``` ```
## Special Keys
There are two special key/value added by Owl if not provided in the environment:
the `QWeb` instance and a `browser` object:
- `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary
for Owl to be able to render anything
- `browser`: this is an object that contains some common access points to the
browser methods with a side effect. This is particularly useful when one want
to test more advanced components, and be able to mock those methods.
More specifically, the `browser` object contains the following methods and objects:
- `setTimeout`
- `clearTimeout`
- `setInterval`
- `clearInterval`
- `requestAnimationFrame`
- `random`
- `Date`
- `fetch`
- `localStorage`
+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;
} }
+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 });
``` ```
+1 -1
View File
@@ -532,7 +532,7 @@ The javascript QWeb implementation provides two useful debugging directives:
```xml ```xml
<t t-if="a_test"> <t t-if="a_test">
<t t-debug=""> <t t-debug=""/>
</t> </t>
``` ```
+6 -3
View File
@@ -37,16 +37,16 @@ rendering context.
</div> </div>
``` ```
Slots are defined by the caller, with the `t-set` directive: Slots are defined by the caller, with the `t-set-slot` directive:
```xml ```xml
<div t-name="SomeComponent"> <div t-name="SomeComponent">
<div>some component</div> <div>some component</div>
<Dialog title="Some Dialog"> <Dialog title="Some Dialog">
<t t-set="content"> <t t-set-slot="content">
<div>hey</div> <div>hey</div>
</t> </t>
<t t-set="footer"> <t t-set-slot="footer">
<button t-on-click="doSomething">ok</button> <button t-on-click="doSomething">ok</button>
</t> </t>
</Dialog> </Dialog>
@@ -57,6 +57,9 @@ In this example, the component `Dialog` will render the slots `content` and `foo
with its parent as rendering context. This means that clicking on the button with its parent as rendering context. This means that clicking on the button
will execute the `doSomething` method on the parent, not on the dialog. will execute the `doSomething` method on the parent, not on the dialog.
Note: Owl previously used the `t-set` directive to define the content of a slot.
This is deprecated and should no longer be used in new code.
## Reference ## Reference
Default slot: the first element inside the component which is not a named slot will Default slot: the first element inside the component which is not a named slot will
+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);
+7 -5
View File
@@ -1,12 +1,13 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "1.0.5", "version": "1.0.9",
"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": "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": {
@@ -48,7 +49,7 @@
"jest-environment-jsdom": "^24.7.1", "jest-environment-jsdom": "^24.7.1",
"live-server": "^1.2.1", "live-server": "^1.2.1",
"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 +78,7 @@
] ]
}, },
"prettier": { "prettier": {
"printWidth": 100 "printWidth": 100,
"endOfLine": "auto"
} }
} }
+3 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉 # 🦉 OWL Roadmap 🦉
- Current version: 1.0.5 - Current version: 1.0.9
- 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
@@ -16,6 +16,8 @@ change!
### 2.x (2020? 2021? 2022?) ### 2.x (2020? 2021? 2022?)
- stop support for `t-set` directive to define the content of a slot
Maybe: Maybe:
- reimplement vdom to use *block* system, like Vue 3, which should make Owl - reimplement vdom to use *block* system, like Vue 3, which should make Owl
+23
View File
@@ -0,0 +1,23 @@
export interface Browser {
setTimeout: Window["setTimeout"];
clearTimeout: Window["clearTimeout"];
setInterval: Window["setInterval"];
clearInterval: Window["clearInterval"];
requestAnimationFrame: Window["requestAnimationFrame"];
random: Math["random"];
Date: typeof Date;
fetch: Window["fetch"];
localStorage: Window["localStorage"];
}
export const browser: Browser = {
setTimeout: window.setTimeout.bind(window),
clearTimeout: window.clearTimeout.bind(window),
setInterval: window.setInterval.bind(window),
clearInterval: window.clearInterval.bind(window),
requestAnimationFrame: window.requestAnimationFrame.bind(window),
random: Math.random,
Date: window.Date,
fetch: (window.fetch || (() => {})).bind(window),
localStorage: window.localStorage,
};
+19 -10
View File
@@ -7,6 +7,7 @@ import { Fiber } from "./fiber";
import "./props_validation"; import "./props_validation";
import { Scheduler, scheduler } from "./scheduler"; import { Scheduler, scheduler } from "./scheduler";
import { activateSheet } from "./styles"; import { activateSheet } from "./styles";
import { Browser, browser } from "../browser";
/** /**
* Owl Component System * Owl Component System
@@ -34,7 +35,7 @@ import { activateSheet } from "./styles";
*/ */
export interface Env { export interface Env {
qweb: QWeb; qweb: QWeb;
[key: string]: any; browser: Browser;
} }
export type MountPosition = "first-child" | "last-child" | "self"; export type MountPosition = "first-child" | "last-child" | "self";
@@ -158,6 +159,9 @@ export class Component<Props extends {} = any, T extends Env = Env> {
if (!this.env.qweb) { if (!this.env.qweb) {
this.env.qweb = new QWeb(); this.env.qweb = new QWeb();
} }
if (!this.env.browser) {
this.env.browser = browser;
}
this.env.qweb.on("update", this, () => { this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) { if (this.__owl__.isMounted) {
this.render(true); this.render(true);
@@ -199,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);
@@ -312,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) {
@@ -357,14 +364,15 @@ export class Component<Props extends {} = any, T extends Env = Env> {
*/ */
async render(force: boolean = false): Promise<void> { async render(force: boolean = false): Promise<void> {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (!__owl__.isMounted && !__owl__.currentFiber) { const currentFiber = __owl__.currentFiber;
if (!__owl__.isMounted && !currentFiber) {
// if we get here, this means that the component was either never mounted, // if we get here, this means that the component was either never mounted,
// or was unmounted and some state change triggered a render. Either way, // or was unmounted and some state change triggered a render. Either way,
// we do not want to actually render anything in this case. // we do not want to actually render anything in this case.
return; return;
} }
if (__owl__.currentFiber && !__owl__.currentFiber.isRendered) { if (currentFiber && !currentFiber.isRendered && !currentFiber.isCompleted) {
return scheduler.addFiber(__owl__.currentFiber.root); return scheduler.addFiber(currentFiber.root);
} }
// if we aren't mounted at this point, it implies that there is a // if we aren't mounted at this point, it implies that there is a
// currentFiber that is already rendered (isRendered is true), so we are // currentFiber that is already rendered (isRendered is true), so we are
@@ -509,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) {
@@ -544,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;
@@ -603,9 +611,10 @@ export class Component<Props extends {} = any, T extends Env = Env> {
// key. So we fall back on looking for a template matching its name (or // key. So we fall back on looking for a template matching its name (or
// one of its subclass). // one of its subclass).
let template: string; let template: string = p.name;
while ((template = p.name) && !(template in qweb.templates) && p !== Component) { while (!(template in qweb.templates) && p !== Component) {
p = p.__proto__; p = p.__proto__;
template = p.name;
} }
if (p === Component) { if (p === Component) {
throw new Error(`Could not find template for component "${this.constructor.name}"`); throw new Error(`Could not find template for component "${this.constructor.name}"`);
@@ -640,7 +649,7 @@ 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
+30 -9
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,15 +400,36 @@ QWeb.addDirective({
if (hasSlots) { if (hasSlots) {
const clone = <Element>node.cloneNode(true); const clone = <Element>node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]"); const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
// 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
// the content of a slot.
//
// This is wrong, but is necessary to prevent breaking all existing Owl
// 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
// slot.
for (let el of clone.children) {
if (el.getAttribute("t-set") && el.hasChildNodes()) {
slotNodes.push(el);
}
}
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); slotNode.parentElement!.removeChild(slotNode);
const key = slotNode.getAttribute("t-set")!; let key = slotNode.getAttribute("t-set-slot")!;
slotNode.removeAttribute("t-set"); slotNode.removeAttribute("t-set-slot");
// here again, this code should be removed when we stop supporting
// using t-set to define the content of named slots.
if (!key) {
key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set");
}
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx); const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
QWeb.slots[`${slotId}_${key}`] = slotFn; QWeb.slots[`${slotId}_${key}`] = slotFn;
} }
@@ -448,5 +469,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;
} },
}); });
+8 -2
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;
}; };
@@ -225,6 +225,12 @@ export class Fiber {
`Cannot attach '${component.constructor.name}' to target node (not same tag name)` `Cannot attach '${component.constructor.name}' to target node (not same tag name)`
); );
} }
// In self mode, we *know* we are to take possession of the target
// Hence we manually create the corresponding VNode and copy the "key" in data
const selfVnodeData = fiber.vnode!.data ? { key: fiber.vnode!.data.key } : {};
const selfVnode = h(fiber.vnode!.sel, selfVnodeData);
selfVnode.elm = target;
target = selfVnode;
} else { } else {
target = component.__owl__.vnode || document.createElement(fiber.vnode!.sel!); target = component.__owl__.vnode || document.createElement(fiber.vnode!.sel!);
} }
@@ -274,7 +280,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--;
} }
+1 -1
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)
+7 -7
View File
@@ -1,4 +1,5 @@
import { Fiber } from "./fiber"; import { Fiber } from "./fiber";
import { browser } from "../browser";
/** /**
* Owl Scheduler Class * Owl Scheduler Class
@@ -19,9 +20,9 @@ interface Task {
export class Scheduler { export class Scheduler {
tasks: Task[] = []; tasks: Task[] = [];
isRunning: boolean = false; isRunning: boolean = false;
requestAnimationFrame: typeof window.requestAnimationFrame; requestAnimationFrame: Window["requestAnimationFrame"];
constructor(requestAnimationFrame) { constructor(requestAnimationFrame: Window["requestAnimationFrame"]) {
this.requestAnimationFrame = requestAnimationFrame; this.requestAnimationFrame = requestAnimationFrame;
} }
@@ -49,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();
@@ -59,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();
@@ -75,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;
@@ -109,5 +110,4 @@ export class Scheduler {
} }
} }
const raf = window.requestAnimationFrame.bind(window); export const scheduler = new Scheduler(browser.requestAnimationFrame);
export const scheduler = new Scheduler(raf);
+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);
} }
} }
+2 -2
View File
@@ -67,14 +67,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);
+13 -13
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,12 +65,12 @@ 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 {
component.__owl__[method] = cb; component.__owl__[method] = cb;
@@ -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);
} }
} },
}); });
} }
/** /**
+22 -15
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();
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -233,10 +234,12 @@ 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
@@ -274,12 +277,16 @@ QWeb.addDirective({
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, ${callingScope}, ${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, ${callingScope}, ${extra});`
);
ctx.addLine(`result = result[0]`); ctx.addLine(`result = result[0]`);
} }
@@ -292,7 +299,7 @@ QWeb.addDirective({
} }
return true; return true;
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -353,7 +360,7 @@ QWeb.addDirective({
ctx.addLine("}"); ctx.addLine("}");
ctx.stopProtectScope(varsID); ctx.stopProtectScope(varsID);
return true; return true;
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -364,7 +371,7 @@ QWeb.addDirective({
priority: 1, priority: 1,
atNodeEncounter({ ctx }) { atNodeEncounter({ ctx }) {
ctx.addLine("debugger;"); ctx.addLine("debugger;");
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -376,5 +383,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})`);
} },
}); });
+6 -5
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,18 +189,19 @@ 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 {
const protectID = this.generateID(); const protectID = this.generateID();
this.rootContext.protectedScopeNumber++; this.rootContext.protectedScopeNumber++;
this.rootContext.shouldDefineScope = true; this.rootContext.shouldDefineScope = true;
const scopeExpr = codeBlock const scopeExpr = `Object.create(scope);`;
? `Object.create(scope);`
: `Object.assign(Object.create(context), scope);`;
this.addLine(`let _origScope${protectID} = scope;`); this.addLine(`let _origScope${protectID} = scope;`);
this.addLine(`scope = ${scopeExpr}`); this.addLine(`scope = ${scopeExpr}`);
if (!codeBlock) {
this.addLine(`scope.__access_mode__ = 'ro';`);
}
return protectID; return protectID;
} }
stopProtectScope(protectID: number) { stopProtectScope(protectID: 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("");
} }
+14 -14
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,13 +251,13 @@ 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;
}; };
@@ -305,7 +305,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 +329,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;
} }
} },
}); });
+33 -23
View File
@@ -77,7 +77,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 {
@@ -112,9 +112,9 @@ const UTILS: Utils = {
addNS(vnode.data, vnode.children, vnode.sel); addNS(vnode.data, vnode.children, vnode.sel);
}, },
VDomArray: class VDomArray extends Array {}, VDomArray: class VDomArray extends Array {},
vDomToString: function(vdom: VNode[]): string { vDomToString: function (vdom: VNode[]): string {
return vdom return vdom
.map(vnode => { .map((vnode) => {
if (vnode.sel) { if (vnode.sel) {
const node = document.createElement(vnode.sel); const node = document.createElement(vnode.sel);
const result = patch(node, vnode); const result = patch(node, vnode);
@@ -133,7 +133,11 @@ const UTILS: Utils = {
}, },
getScope(obj, property: string) { getScope(obj, property: string) {
const obj0 = obj; const obj0 = obj;
while (obj && !obj.hasOwnProperty(property)) { while (
obj &&
!obj.hasOwnProperty(property) &&
!(obj.hasOwnProperty("__access_mode__") && obj.__access_mode__ === "ro")
) {
const newObj = obj.__proto__; const newObj = obj.__proto__;
if (!newObj || isComponent(newObj)) { if (!newObj || isComponent(newObj)) {
return obj0; return obj0;
@@ -141,7 +145,7 @@ const UTILS: Utils = {
obj = newObj; obj = newObj;
} }
return obj; return obj;
} },
}; };
function parseXML(xml: string): Document { function parseXML(xml: string): Document {
@@ -191,7 +195,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[] = [];
@@ -208,10 +212,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"];
@@ -235,7 +241,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));
} }
} }
@@ -299,11 +305,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;
} }
@@ -313,10 +319,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"))) {
@@ -326,7 +332,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
) { ) {
@@ -578,7 +584,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) {
@@ -593,7 +599,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);
}; };
@@ -607,7 +613,7 @@ export class QWeb extends EventBus {
fullName, fullName,
value, value,
nodeID, nodeID,
addNodeHook addNodeHook,
}); });
} }
} }
@@ -698,10 +704,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)}';`);
@@ -742,7 +752,7 @@ 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};`);
+2 -1
View File
@@ -1,7 +1,8 @@
import { Component } from "../component/component"; import { Component } from "../component/component";
import { xml } from "../tags"; import { xml } from "../tags";
import { EnvWithRouter } from "./router";
export class RouteComponent extends Component { export class RouteComponent extends Component<{}, EnvWithRouter> {
static template = xml` static template = xml`
<t> <t>
<t <t
+12 -4
View File
@@ -49,6 +49,10 @@ interface Options {
mode: Router["mode"]; mode: Router["mode"];
} }
export interface EnvWithRouter extends Env {
router: Router;
}
const paramRegexp = /\{\{(.*?)\}\}/; const paramRegexp = /\{\{(.*?)\}\}/;
export class Router { export class Router {
@@ -60,7 +64,11 @@ export class Router {
routeIds: string[]; routeIds: string[];
env: RouterEnv; env: RouterEnv;
constructor(env: Env, routes: Partial<Route>[], options: Options = { mode: "history" }) { constructor(
env: Partial<EnvWithRouter>,
routes: Partial<Route>[],
options: Options = { mode: "history" }
) {
env.router = this; env.router = this;
this.mode = options.mode; this.mode = options.mode;
this.env = env as RouterEnv; this.env = env as RouterEnv;
@@ -89,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);
@@ -190,7 +198,7 @@ export class Router {
return { return {
type: "match", type: "match",
route: route, route: route,
params: params params: params,
}; };
} }
} }
@@ -215,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" };
+14 -10
View File
@@ -24,6 +24,10 @@ import { onWillUpdateProps } from "./hooks";
// Store Definition // Store Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
export interface EnvWithStore extends Env {
store: Store;
}
export type Action = ({ state, dispatch, env, getters }, ...payload: any) => any; export type Action = ({ state, dispatch, env, getters }, ...payload: any) => any;
export type Getter = ({ state: any, getters }, payload?) => any; export type Getter = ({ state: any, getters }, payload?) => any;
@@ -49,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);
@@ -66,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
); );
@@ -83,7 +87,7 @@ interface SelectorOptions {
const isStrictEqual = (a, b) => a === b; const isStrictEqual = (a, b) => a === b;
export function useStore(selector, options: SelectorOptions = {}): any { export function useStore(selector, options: SelectorOptions = {}): any {
const component: Component = Component.current!; const component = Component.current as Component<any, EnvWithStore>;
const componentId = component.__owl__.id; const componentId = component.__owl__.id;
const store = options.store || (component.env.store as Store); const store = options.store || (component.env.store as Store);
if (!(store instanceof Store)) { if (!(store instanceof Store)) {
@@ -109,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;
@@ -122,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);
}; };
@@ -144,16 +148,16 @@ export function useStore(selector, options: SelectorOptions = {}): any {
}, },
has(target, k) { has(target, k) {
return k in result; return k in result;
} },
}); });
} }
export function useDispatch(store?: Store): Store["dispatch"] { export function useDispatch(store?: Store): Store["dispatch"] {
store = store || (Component.current!.env.store as Store); store = store || (Component.current!.env as EnvWithStore).store;
return store.dispatch.bind(store); return store.dispatch.bind(store);
} }
export function useGetters(store?: Store): Store["getters"] { export function useGetters(store?: Store): Store["getters"] {
store = store || (Component.current!.env.store as Store); store = store || (Component.current!.env as EnvWithStore).store;
return store.getters; return store.getters;
} }
+11 -9
View File
@@ -10,14 +10,16 @@
* - debounce * - debounce
*/ */
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> } = {};
@@ -26,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];
@@ -44,7 +46,7 @@ export function loadJS(url: string): Promise<void> {
} }
export async function loadFile(url: string): Promise<string> { export async function loadFile(url: string): Promise<string> {
const result = await fetch(url); const result = await browser.fetch(url);
if (!result.ok) { if (!result.ok) {
throw new Error("Error while fetching xml templates"); throw new Error("Error while fetching xml templates");
} }
@@ -73,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() {
@@ -83,8 +85,8 @@ export function debounce(func: Function, wait: number, immediate?: boolean): Fun
} }
} }
const callNow = immediate && !timeout; const callNow = immediate && !timeout;
clearTimeout(timeout); browser.clearTimeout(timeout);
timeout = setTimeout(later, wait); timeout = browser.setTimeout(later, wait);
if (callNow) { if (callNow) {
func.apply(context, args); func.apply(context, args);
} }
+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,7 +1253,7 @@ 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__'})); this.constructor.subTemplates['2'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'}));
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
let vn5 = h('p', p5, c5); let vn5 = h('p', p5, c5);
c1.push(vn5); c1.push(vn5);
@@ -1282,13 +1282,14 @@ exports[`other directives with t-component t-set can't alter in t-call body 1`]
} }
{ {
let _origScope4 = scope; let _origScope4 = scope;
scope = Object.assign(Object.create(context), 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};
@@ -1409,7 +1410,7 @@ 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__'})); this.constructor.subTemplates['3'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'}));
return vn1; return vn1;
}" }"
`; `;
@@ -90,6 +90,96 @@ exports[`t-slot directive can define and call slots 4`] = `
}" }"
`; `;
exports[`t-slot directive can define and call slots using old t-set keyword 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);
// Component 'Dialog'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Dialog\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Dialog'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`t-slot directive can define and call slots using old t-set keyword 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let h = this.h;
let c6 = [], p6 = {key:6};
let vn6 = h('div', p6, c6);
let c7 = [], p7 = {key:7};
let vn7 = h('div', p7, c7);
c6.push(vn7);
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot8) {
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context}));
}
let c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9);
c6.push(vn9);
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot10) {
slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
}
return vn6;
}"
`;
exports[`t-slot directive can define and call slots using old t-set keyword 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_header_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`header\`});
}"
`;
exports[`t-slot directive can define and call slots using old t-set keyword 4`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`footer\`});
}"
`;
exports[`t-slot directive content is the default slot 1`] = ` exports[`t-slot directive content is the default slot 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -469,6 +559,21 @@ exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
}" }"
`; `;
exports[`t-slot directive t-set t-value in a slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let h = this.h;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot5) {
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context}));
}
return vn4;
}"
`;
exports[`t-slot directive template can just return a slot 1`] = ` exports[`t-slot directive template can just return a slot 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
+24 -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 {
@@ -1366,6 +1366,28 @@ describe("async rendering", () => {
await prom; await prom;
}); });
test("concurrent renderings scenario 17", async () => {
class Parent extends Component {
static template = xml`<span><t t-esc="state.value"/></span>`;
state = useState({ value: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<span>1</span>");
parent.state.value = 2;
parent.__owl__.currentFiber!.cancel();
parent.state.value = 3; // update value directly
await nextTick();
expect(fixture.innerHTML).toBe("<span>3</span>");
parent.state.value = 4; // update value after a tick
await nextTick();
expect(fixture.innerHTML).toBe("<span>4</span>");
});
test("change state and call manually render: no unnecessary rendering", async () => { test("change state and call manually render: no unnecessary rendering", async () => {
class Widget extends Component { class Widget extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`; static template = xml`<div><t t-esc="state.val"/></div>`;
+55 -34
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 };
} }
@@ -2003,7 +2003,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 +2038,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 +2976,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 +2999,7 @@ describe("random stuff/miscellaneous", () => {
"E:destroy", "E:destroy",
"F:mounted", "F:mounted",
"D:patched", "D:patched",
"C:patched" "C:patched",
]); ]);
}); });
@@ -3080,6 +3080,15 @@ describe("can deduce template from name", () => {
expect(fixture.innerHTML).toBe("<span>Orval</span>"); expect(fixture.innerHTML).toBe("<span>Orval</span>");
}); });
test("can find template of anonymous component", async () => {
class ABC extends Component {}
const Anonymous = class extends ABC {};
env.qweb.addTemplate("ABC", "<span>Orval</span>");
const def = new Anonymous();
await def.mount(fixture);
expect(fixture.innerHTML).toBe("<span>Orval</span>");
});
test("can find template of parent component, defined by template key", async () => { test("can find template of parent component, defined by template key", async () => {
class ABC extends Component { class ABC extends Component {
static template = "Achel"; static template = "Achel";
@@ -3401,7 +3410,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();
@@ -3446,7 +3455,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;
@@ -3475,11 +3484,14 @@ describe("environment and plugins", () => {
}); });
test("can define specific env for root components", async () => { test("can define specific env for root components", async () => {
class App1 extends Component { interface AppEnv extends Env {
test: number;
}
class App1 extends Component<{}, AppEnv> {
static template = xml`<span></span>`; static template = xml`<span></span>`;
} }
App1.env = { test: 1 }; App1.env = { test: 1 };
class App2 extends Component { class App2 extends Component<{}, AppEnv> {
static template = xml`<span></span>`; static template = xml`<span></span>`;
} }
App2.env = { test: 2 }; App2.env = { test: 2 };
@@ -3813,7 +3825,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 () => {
@@ -3835,7 +3849,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 () => {
@@ -3877,7 +3894,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 () => {
@@ -3893,7 +3912,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();
}); });
+15 -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;
+71 -15
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]);
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -41,8 +41,8 @@ describe("t-slot directive", () => {
<templates> <templates>
<div t-name="Parent"> <div t-name="Parent">
<Dialog> <Dialog>
<t t-set="header"><span>header</span></t> <t t-set-slot="header"><span>header</span></t>
<t t-set="footer"><span>footer</span></t> <t t-set-slot="footer"><span>footer</span></t>
</Dialog> </Dialog>
</div> </div>
<div t-name="Dialog"> <div t-name="Dialog">
@@ -67,6 +67,38 @@ describe("t-slot directive", () => {
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot(); expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
}); });
test("can define and call slots using old t-set keyword", async () => {
// NOTE: this test should be removed once we stop supporting the t-set directive
// for defining slot content.
class Dialog extends Component {
static template = xml`
<div>
<div><t t-slot="header"/></div>
<div><t t-slot="footer"/></div>
</div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Dialog>
<t t-set="header"><span>header</span></t>
<t t-set="footer"><span>footer</span></t>
</Dialog>
</div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div><div><span>header</span></div><div><span>footer</span></div></div></div>"
);
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot();
expect(QWeb.slots["1_header"].toString()).toMatchSnapshot();
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("named slots can define a default content", async () => { test("named slots can define a default content", async () => {
class Dialog extends Component { class Dialog extends Component {
static template = xml` static template = xml`
@@ -128,7 +160,7 @@ describe("t-slot directive", () => {
</span>`; </span>`;
} }
class Parent extends Component { class Parent extends Component {
static template = xml`<div><Dialog><t t-set="header">hey</t></Dialog></div>`; static template = xml`<div><Dialog><t t-set-slot="header">hey</t></Dialog></div>`;
static components = { Dialog }; static components = { Dialog };
} }
const parent = new Parent(); const parent = new Parent();
@@ -143,7 +175,7 @@ describe("t-slot directive", () => {
<div t-name="Parent"> <div t-name="Parent">
<span class="counter"><t t-esc="state.val"/></span> <span class="counter"><t t-esc="state.val"/></span>
<Dialog> <Dialog>
<t t-set="footer"><button t-on-click="doSomething">do something</button></t> <t t-set-slot="footer"><button t-on-click="doSomething">do something</button></t>
</Dialog> </Dialog>
</div> </div>
<span t-name="Dialog"><t t-slot="footer"/></span> <span t-name="Dialog"><t t-slot="footer"/></span>
@@ -192,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 };
} }
@@ -236,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 };
} }
@@ -302,7 +334,7 @@ describe("t-slot directive", () => {
<div> <div>
<span class="counter"><t t-esc="state.val"/></span> <span class="counter"><t t-esc="state.val"/></span>
<Dialog> <Dialog>
<t t-set="footer"><button t-ref="myButton" t-on-click="doSomething">do something</button></t> <t t-set-slot="footer"><button t-ref="myButton" t-on-click="doSomething">do something</button></t>
</Dialog> </Dialog>
</div> </div>
`; `;
@@ -376,7 +408,7 @@ describe("t-slot directive", () => {
<templates> <templates>
<div t-name="Parent"> <div t-name="Parent">
<Dialog> <Dialog>
<t t-set="content"> <t t-set-slot="content">
<span>sts</span> <span>sts</span>
<span>rocks</span> <span>rocks</span>
</t> </t>
@@ -442,14 +474,14 @@ describe("t-slot directive", () => {
expect(fixture.innerHTML).toBe("<div><span><span>some content</span></span></div>"); expect(fixture.innerHTML).toBe("<div><span><span>some content</span></span></div>");
}); });
test("t-debug on a t-set (defining a slot)", async () => { test("t-debug on a t-set-slot (defining a slot)", async () => {
const consoleLog = console.log; const consoleLog = console.log;
console.log = jest.fn(); console.log = jest.fn();
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
<div t-name="Parent"> <div t-name="Parent">
<Dialog><t t-set="content" t-debug="">abc</t></Dialog> <Dialog><t t-set-slot="content" t-debug="">abc</t></Dialog>
</div> </div>
<span t-name="Dialog"> <span t-name="Dialog">
<t t-slot="content"/> <t t-slot="content"/>
@@ -623,8 +655,8 @@ describe("t-slot directive", () => {
class A extends Component { class A extends Component {
static template = xml` static template = xml`
<B> <B>
<t t-set="s1"><C val="1"/></t> <t t-set-slot="s1"><C val="1"/></t>
<t t-set="s2"><C val="2"/></t> <t t-set-slot="s2"><C val="2"/></t>
</B>`; </B>`;
static components = { B, C }; static components = { B, C };
} }
@@ -873,4 +905,28 @@ describe("t-slot directive", () => {
await parent.mount(fixture); await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span><p>heyaaron</p></span></div></div>"); expect(fixture.innerHTML).toBe("<div><div><span><p>heyaaron</p></span></div></div>");
}); });
test("t-set t-value in a slot", 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-set="rainbow" t-value="'dash'"/>
<t t-esc="rainbow"/>
</Dialog>
</div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>dash</span></div>");
expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot();
});
}); });
+42 -3
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
@@ -28,14 +29,34 @@ afterEach(() => {
describe("mount targets", () => { describe("mount targets", () => {
test("can attach a component to an existing node (if same tagname)", async () => { test("can attach a component to an existing node (if same tagname)", async () => {
class App extends Component { class App extends Component {
static template = xml`<div>app</div>`; static template = xml`<div t-att-class="state.customClass">app<p>another tag</p></div>`;
state = useState({ customClass: "custom" });
} }
const div = document.createElement("div"); const div = document.createElement("div");
div.classList.add("arbitrary");
div.innerHTML = `<p>pre-existing</p>`;
fixture.appendChild(div); fixture.appendChild(div);
const app = new App(); const app = new App();
await app.mount(div, { position: "self" }); await app.mount(div, { position: "self" });
expect(fixture.innerHTML).toBe("<div>app</div>");
expect(fixture.innerHTML).toBe(
`<div class="arbitrary custom"><p>pre-existing</p>app<p>another tag</p></div>`
);
expect(div).toBe(app.el);
app.state.customClass = "custom2";
await nextTick();
expect(fixture.innerHTML).toBe(
`<div class="arbitrary custom2"><p>pre-existing</p>app<p>another tag</p></div>`
);
expect(div).toBe(app.el);
app.unmount();
// This assert is a best guess
// The use case it covers was not really thought through
// and may change in the future
expect(fixture.innerHTML).toBe("");
}); });
test("cannot attach a component to an existing node (if not same tagname)", async () => { test("cannot attach a component to an existing node (if not same tagname)", async () => {
@@ -315,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);
@@ -470,4 +491,22 @@ 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");
});
}); });
+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");
+2 -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,
}); });
}); });
+5 -3
View File
@@ -6,9 +6,10 @@ import { patch } from "../src/vdom";
import "../src/qweb/base_directives"; import "../src/qweb/base_directives";
import "../src/qweb/extensions"; import "../src/qweb/extensions";
import "../src/component/directive"; import "../src/component/directive";
import { browser } from "../src/browser";
// modifies scheduler to make it faster to test components // modifies scheduler to make it faster to test components
scheduler.requestAnimationFrame = function(callback: FrameRequestCallback) { scheduler.requestAnimationFrame = function (callback: FrameRequestCallback) {
setTimeout(callback, 1); setTimeout(callback, 1);
return 1; return 1;
}; };
@@ -42,7 +43,7 @@ export function nextMicroTick(): Promise<void> {
} }
export async function nextTick(): Promise<void> { export async function nextTick(): Promise<void> {
return new Promise(function(resolve) { return new Promise(function (resolve) {
setTimeout(() => scheduler.requestAnimationFrame(() => resolve())); setTimeout(() => scheduler.requestAnimationFrame(() => resolve()));
}); });
} }
@@ -75,7 +76,8 @@ export function makeDeferred(): Deferred {
export function makeTestEnv(): Env { export function makeTestEnv(): Env {
return { return {
qweb: new QWeb() qweb: new QWeb(),
browser: browser,
}; };
} }
+40 -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();
@@ -559,20 +559,39 @@ 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> {
return new Promise((resolve) => {
setTimeout(() => {
resolve("slow");
}, 0);
});
}
function useMyHook() { function useMyHook() {
onWillStart(() => { onWillStart(async () => {
steps.push(await slow());
steps.push("onWillStart"); steps.push("onWillStart");
}); });
onWillUpdateProps(nextProps => { onWillUpdateProps(async (nextProps) => {
expect(nextProps).toEqual({ value: 2 }); expect(nextProps).toEqual({ value: 2 });
steps.push(await slow());
steps.push("onWillUpdateProps"); steps.push("onWillUpdateProps");
}); });
} }
function use2ndHook() {
onWillStart(() => {
steps.push("on2ndStart");
});
onWillUpdateProps((nextProps) => {
expect(nextProps).toEqual({ value: 2 });
steps.push("on2ndUpdate");
});
}
class MyComponent extends Component { class MyComponent extends Component {
static template = xml`<span><t t-esc="props.value"/></span>`; static template = xml`<span><t t-esc="props.value"/></span>`;
constructor(parent, props) { constructor(parent, props) {
super(parent, props); super(parent, props);
useMyHook(); useMyHook();
use2ndHook();
} }
} }
class App extends Component { class App extends Component {
@@ -586,12 +605,26 @@ describe("hooks", () => {
expect(app).not.toHaveProperty("willStart"); expect(app).not.toHaveProperty("willStart");
expect(app).not.toHaveProperty("willUpdateProps"); expect(app).not.toHaveProperty("willUpdateProps");
expect(fixture.innerHTML).toBe("<div><span>1</span></div>"); expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
expect(steps).toEqual(["onWillStart"]);
// NOTE: 'on2ndStart' appears first in the list even though
// the 'use2ndHook' is declared after 'useMyHook'. This is
// because Promise.all is used to call the callbacks specified
// in the hooks, which runs them simultaneously.
// Additionally, 'slow' should be listed before 'onWillStart'
// because call to `slow` is awaited.
expect(steps).toEqual(["on2ndStart", "slow", "onWillStart"]);
app.state.value = 2; app.state.value = 2;
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div><span>2</span></div>"); expect(fixture.innerHTML).toBe("<div><span>2</span></div>");
expect(steps).toEqual(["onWillStart", "onWillUpdateProps"]); expect(steps).toEqual([
"on2ndStart",
"slow",
"onWillStart",
"on2ndUpdate",
"slow",
"onWillUpdateProps",
]);
}); });
test("useExternalListener", async () => { test("useExternalListener", async () => {
+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);
+227 -113
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
) { ) {
@@ -734,7 +749,7 @@ 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__'})); this.constructor.subTemplates['1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'}));
return vn1; return vn1;
}" }"
`; `;
@@ -774,35 +789,37 @@ exports[`misc global 1`] = `
} }
{ {
let _origScope10 = scope; let _origScope10 = scope;
scope = Object.assign(Object.create(context), scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{ {
let c__0 = []; let c__0 = [];
{ {
let _origScope13 = scope; let _origScope13 = scope;
scope = Object.assign(Object.create(context), 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; scope = _origScope13;
} }
let k15 = \`__15__\${key1}__\`; let k15 = \`__15__\${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: k15})); this.constructor.subTemplates['2'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c__0, parent: utils.getComponent(context), key: k15}));
utils.getScope(scope, 'foo').foo = 'bbb'; utils.getScope(scope, 'foo').foo = 'bbb';
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, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c__0, parent: utils.getComponent(context), key: k16}));
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
let k17 = \`__17__\${key1}__\`; let k17 = \`__17__\${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: k17}));
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__'})); this.constructor.subTemplates['3'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__20__'}));
return vn1; return vn1;
}" }"
`; `;
@@ -1091,25 +1108,11 @@ 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__'})); this.constructor.subTemplates['1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__3__'}));
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
) { ) {
@@ -1119,26 +1122,12 @@ exports[`t-call (template calling basic caller, no parent node 1`] = `
let result; let result;
let h = this.h; let h = this.h;
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, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: result, parent: utils.getComponent(context), key: '__3__'}));
result = result[0] result = result[0]
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,7 +1139,8 @@ exports[`t-call (template calling call with several sub nodes on same line 1`] =
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
{ {
let _origScope3 = scope; let _origScope3 = scope;
scope = Object.assign(Object.create(context), 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};
@@ -1164,28 +1154,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
@@ -1198,7 +1174,8 @@ exports[`t-call (template calling cascading t-call t-raw='0' 1`] = `
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
{ {
let _origScope12 = scope; let _origScope12 = scope;
scope = Object.assign(Object.create(context), 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};
@@ -1212,7 +1189,7 @@ 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;
@@ -1229,7 +1206,7 @@ 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__'})); this.constructor.subTemplates['1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__2__'}));
return vn1; return vn1;
}" }"
`; `;
@@ -1248,7 +1225,7 @@ 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__'})); this.constructor.subTemplates['1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__6__'}));
} }
return vn1; return vn1;
}" }"
@@ -1272,7 +1249,7 @@ exports[`t-call (template calling recursive template, part 1 2`] = `
c4.push({text: \`hey\`}); c4.push({text: \`hey\`});
if (false) { if (false) {
let k5 = \`__5__\${key0}__\`; let k5 = \`__5__\${key0}__\`;
this.subTemplates['recursive'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c3, parent: utils.getComponent(context), key: k5})); this.constructor.subTemplates['1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c3, parent: utils.getComponent(context), key: k5}));
} }
}" }"
`; `;
@@ -1288,13 +1265,14 @@ exports[`t-call (template calling recursive template, part 2 1`] = `
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
{ {
let _origScope11 = scope; let _origScope11 = scope;
scope = Object.assign(Object.create(context), 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;
@@ -1339,14 +1317,15 @@ exports[`t-call (template calling recursive template, part 2 2`] = `
let key1 = i1; let key1 = i1;
{ {
let _origScope9 = scope; let _origScope9 = scope;
scope = Object.assign(Object.create(context), 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;
} }
} }
@@ -1365,13 +1344,14 @@ exports[`t-call (template calling recursive template, part 3 1`] = `
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
{ {
let _origScope11 = scope; let _origScope11 = scope;
scope = Object.assign(Object.create(context), 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;
@@ -1416,14 +1396,100 @@ exports[`t-call (template calling recursive template, part 3 2`] = `
let key1 = i1; let key1 = i1;
{ {
let _origScope9 = scope; let _origScope9 = scope;
scope = Object.assign(Object.create(context), 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 = _origScope8;
}"
`;
exports[`t-call (template calling recursive template, part 4: with t-set recursive index 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"Parent\\"
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 _origScope11 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
utils.getScope(scope, 'recursive_idx').recursive_idx = 1;
utils.getScope(scope, 'node').node = scope['root'];
scope[utils.zero] = c__0;
}
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__12__'}));
scope = _origScope11;
}
return vn1;
}"
`;
exports[`t-call (template calling recursive template, part 4: with t-set recursive index 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"nodeTemplate\\"
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);
scope.recursive_idx = scope.recursive_idx+1;
let c3 = [], p3 = {key:\`\${key0}_3\`};
let vn3 = h('p', p3, c3);
c2.push(vn3);
let _4 = scope['node'].val;
if (_4 != null) {
c3.push({text: _4});
}
c3.push({text: \` \`});
if (scope.recursive_idx != null) {
c3.push({text: scope.recursive_idx});
}
let _5 = scope['node'].children||[];
if (!_5) { throw new Error('QWeb error: Invalid loop expression')}
let _6 = _7 = _5;
if (!(_5 instanceof Array)) {
_6 = Object.keys(_5);
_7 = Object.values(_5);
}
let _length6 = _6.length;
let _origScope8 = scope;
scope = Object.create(scope);
for (let i1 = 0; i1 < _length6; i1++) {
scope.subtree_first = i1 === 0
scope.subtree_last = i1 === _length6 - 1
scope.subtree_index = i1
scope.subtree = _6[i1]
scope.subtree_value = _7[i1]
let key1 = i1;
{
let _origScope9 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let c__0 = [];
utils.getScope(scope, 'node').node = scope['subtree'];
scope[utils.zero] = c__0;
}
let k10 = \`__10__\${key0}__\${key1}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c2, parent: utils.getComponent(context), key: k10}));
scope = _origScope9; scope = _origScope9;
} }
} }
@@ -1442,13 +1508,14 @@ exports[`t-call (template calling scoped parameters 1`] = `
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
{ {
let _origScope2 = scope; let _origScope2 = scope;
scope = Object.assign(Object.create(context), 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) {
@@ -1468,26 +1535,12 @@ 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__'})); this.constructor.subTemplates['1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__3__'}));
} }
return vn1; return vn1;
}" }"
`; `;
exports[`t-call (template calling t-call with t-if 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"sub\\"
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 t-call with t-set inside and outside 1`] = ` exports[`t-call (template calling t-call with t-set inside and outside 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -1517,14 +1570,15 @@ exports[`t-call (template calling t-call with t-set inside and outside 1`] = `
utils.getScope(scope, 'val').val = scope['v'].val; utils.getScope(scope, 'val').val = scope['v'].val;
{ {
let _origScope8 = scope; let _origScope8 = scope;
scope = Object.assign(Object.create(context), 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;
} }
} }
@@ -1543,7 +1597,7 @@ 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__'})); this.constructor.subTemplates['1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__11__'}));
return vn1; return vn1;
}" }"
`; `;
@@ -1559,18 +1613,19 @@ 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__'})); this.constructor.subTemplates['1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__3__'}));
} }
else if (scope.v1==='elif') { else if (scope.v1==='elif') {
{ {
let _origScope6 = scope; let _origScope6 = scope;
scope = Object.assign(Object.create(context), 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: '__7__'}));
scope = _origScope6; scope = _origScope6;
} }
} }
@@ -1587,7 +1642,35 @@ 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__'})); this.constructor.subTemplates['1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__3__'}));
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);
this.constructor.subTemplates['1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__3__'}));
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);
this.constructor.subTemplates['1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__3__'}));
return vn1; return vn1;
}" }"
`; `;
@@ -1602,14 +1685,15 @@ exports[`t-call (template calling with unused body 1`] = `
let h = this.h; let h = this.h;
{ {
let _origScope3 = scope; let _origScope3 = scope;
scope = Object.assign(Object.create(context), 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;
} }
@@ -1627,14 +1711,15 @@ exports[`t-call (template calling with unused setbody 1`] = `
let h = this.h; let h = this.h;
{ {
let _origScope3 = scope; let _origScope3 = scope;
scope = Object.assign(Object.create(context), 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;
} }
@@ -1652,14 +1737,15 @@ exports[`t-call (template calling with used body 1`] = `
let h = this.h; let h = this.h;
{ {
let _origScope3 = scope; let _origScope3 = scope;
scope = Object.assign(Object.create(context), 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;
} }
@@ -1678,13 +1764,14 @@ exports[`t-call (template calling with used set body 1`] = `
let vn1 = h('span', p1, c1); let vn1 = h('span', p1, c1);
{ {
let _origScope3 = scope; let _origScope3 = scope;
scope = Object.assign(Object.create(context), 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;
@@ -1822,6 +1909,23 @@ 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
this.constructor.subTemplates['1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'}));
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
) { ) {
@@ -1839,7 +1943,8 @@ exports[`t-esc t-esc is escaped 1`] = `
scope.var = c2 scope.var = c2
if (scope.var != null) { if (scope.var != null) {
let _origScope4 = scope; let _origScope4 = scope;
scope = Object.assign(Object.create(context), scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.var = scope.var instanceof utils.VDomArray ? utils.vDomToString(scope.var) : scope.var; scope.var = scope.var instanceof utils.VDomArray ? utils.vDomToString(scope.var) : scope.var;
c1.push({text: scope.var}); c1.push({text: scope.var});
scope = _origScope4; scope = _origScope4;
@@ -1875,7 +1980,8 @@ exports[`t-esc t-esc=0 is escaped 1`] = `
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
{ {
let _origScope3 = scope; let _origScope3 = scope;
scope = Object.assign(Object.create(context), 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};
@@ -1884,7 +1990,7 @@ 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;
@@ -2680,7 +2786,7 @@ 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__'})); this.constructor.subTemplates['1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'}));
return vn1; return vn1;
}" }"
`; `;
@@ -2694,7 +2800,7 @@ 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__'})); this.constructor.subTemplates['1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'}));
return vn1; return vn1;
}" }"
`; `;
@@ -2948,7 +3054,8 @@ exports[`t-set set from body literal 1`] = `
scope.value = c1 scope.value = c1
if (scope.value != null) { if (scope.value != null) {
let _origScope2 = scope; let _origScope2 = scope;
scope = Object.assign(Object.create(context), scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.value = scope.value instanceof utils.VDomArray ? utils.vDomToString(scope.value) : scope.value; scope.value = scope.value instanceof utils.VDomArray ? utils.vDomToString(scope.value) : scope.value;
let vn3 = {text: scope.value}; let vn3 = {text: scope.value};
result = vn3 result = vn3
@@ -2975,7 +3082,8 @@ exports[`t-set set from body lookup 1`] = `
scope.stuff = c2 scope.stuff = c2
if (scope.stuff != null) { if (scope.stuff != null) {
let _origScope4 = scope; let _origScope4 = scope;
scope = Object.assign(Object.create(context), scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.stuff = scope.stuff instanceof utils.VDomArray ? utils.vDomToString(scope.stuff) : scope.stuff; scope.stuff = scope.stuff instanceof utils.VDomArray ? utils.vDomToString(scope.stuff) : scope.stuff;
c1.push({text: scope.stuff}); c1.push({text: scope.stuff});
scope = _origScope4; scope = _origScope4;
@@ -3123,7 +3231,8 @@ exports[`t-set t-set with content and sub t-esc 1`] = `
scope.setvar = c2 scope.setvar = c2
if (scope.setvar != null) { if (scope.setvar != null) {
let _origScope4 = scope; let _origScope4 = scope;
scope = Object.assign(Object.create(context), scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.setvar = scope.setvar instanceof utils.VDomArray ? utils.vDomToString(scope.setvar) : scope.setvar; scope.setvar = scope.setvar instanceof utils.VDomArray ? utils.vDomToString(scope.setvar) : scope.setvar;
c1.push({text: scope.setvar}); c1.push({text: scope.setvar});
scope = _origScope4; scope = _origScope4;
@@ -3215,7 +3324,8 @@ exports[`t-set t-set, t-if, and mix of expression/body lookup, 1 1`] = `
} }
if (scope.ourvar != null) { if (scope.ourvar != null) {
let _origScope3 = scope; let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.ourvar = scope.ourvar instanceof utils.VDomArray ? utils.vDomToString(scope.ourvar) : scope.ourvar; scope.ourvar = scope.ourvar instanceof utils.VDomArray ? utils.vDomToString(scope.ourvar) : scope.ourvar;
c1.push({text: scope.ourvar}); c1.push({text: scope.ourvar});
scope = _origScope3; scope = _origScope3;
@@ -3243,7 +3353,8 @@ exports[`t-set t-set, t-if, and mix of expression/body lookup, 1 2`] = `
} }
if (scope.ourvar != null) { if (scope.ourvar != null) {
let _origScope3 = scope; let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.ourvar = scope.ourvar instanceof utils.VDomArray ? utils.vDomToString(scope.ourvar) : scope.ourvar; scope.ourvar = scope.ourvar instanceof utils.VDomArray ? utils.vDomToString(scope.ourvar) : scope.ourvar;
c1.push({text: scope.ourvar}); c1.push({text: scope.ourvar});
scope = _origScope3; scope = _origScope3;
@@ -3271,7 +3382,8 @@ exports[`t-set t-set, t-if, and mix of expression/body lookup, 2 1`] = `
} }
if (scope.ourvar != null) { if (scope.ourvar != null) {
let _origScope3 = scope; let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.ourvar = scope.ourvar instanceof utils.VDomArray ? utils.vDomToString(scope.ourvar) : scope.ourvar; scope.ourvar = scope.ourvar instanceof utils.VDomArray ? utils.vDomToString(scope.ourvar) : scope.ourvar;
c1.push({text: scope.ourvar}); c1.push({text: scope.ourvar});
scope = _origScope3; scope = _origScope3;
@@ -3299,7 +3411,8 @@ exports[`t-set t-set, t-if, and mix of expression/body lookup, 2 2`] = `
} }
if (scope.ourvar != null) { if (scope.ourvar != null) {
let _origScope3 = scope; let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.ourvar = scope.ourvar instanceof utils.VDomArray ? utils.vDomToString(scope.ourvar) : scope.ourvar; scope.ourvar = scope.ourvar instanceof utils.VDomArray ? utils.vDomToString(scope.ourvar) : scope.ourvar;
c1.push({text: scope.ourvar}); c1.push({text: scope.ourvar});
scope = _origScope3; scope = _origScope3;
@@ -3325,7 +3438,8 @@ exports[`t-set value priority 1`] = `
} }
if (scope.value != null) { if (scope.value != null) {
let _origScope3 = scope; let _origScope3 = scope;
scope = Object.assign(Object.create(context), scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro';
scope.value = scope.value instanceof utils.VDomArray ? utils.vDomToString(scope.value) : scope.value; scope.value = scope.value instanceof utils.VDomArray ? utils.vDomToString(scope.value) : scope.value;
c1.push({text: scope.value}); c1.push({text: scope.value});
scope = _origScope3; scope = _origScope3;
+109 -49
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,7 +763,7 @@ 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 not allowed on a non t node", () => {
@@ -885,7 +898,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 +927,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 +955,41 @@ 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();
});
test("recursive template, part 4: with t-set recursive index", () => {
qweb.addTemplates(`
<templates>
<div t-name="Parent">
<t t-call="nodeTemplate">
<t t-set="recursive_idx" t-value="1"/>
<t t-set="node" t-value="root"/>
</t>
</div>
<div t-name="nodeTemplate">
<t t-set="recursive_idx" t-value="recursive_idx + 1"/>
<p><t t-esc="node.val"/> <t t-esc="recursive_idx"/></p>
<t t-foreach="node.children or []" t-as="subtree">
<t t-call="nodeTemplate">
<t t-set="node" t-value="subtree"/>
</t>
</t>
</div>
</templates>
`);
const root = {
val: "a",
children: [{ val: "b", children: [{ val: "c", children: [{ val: "d" }] }] }],
};
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>";
expect(renderToString(qweb, "Parent", { root })).toBe(expected);
const subId = qweb.subTemplates["nodeTemplate"];
const recursiveFn = QWeb.subTemplates[subId] as any;
expect(recursiveFn.toString()).toMatchSnapshot(); expect(recursiveFn.toString()).toMatchSnapshot();
}); });
@@ -1012,6 +1061,17 @@ 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>");
});
}); });
describe("foreach", () => { describe("foreach", () => {
@@ -1197,7 +1257,7 @@ describe("t-on", () => {
{ {
add() { add() {
a = 3; a = 3;
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1220,7 +1280,7 @@ describe("t-on", () => {
}, },
handleDblClick() { handleDblClick() {
steps.push("dblclick"); steps.push("dblclick");
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1240,7 +1300,7 @@ describe("t-on", () => {
{ {
add(n) { add(n) {
a = a + n; a = a + n;
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1257,7 +1317,7 @@ describe("t-on", () => {
{ {
add({ val }) { add({ val }) {
a = a + val; a = a + val;
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1274,7 +1334,7 @@ describe("t-on", () => {
{ {
doSomething(arg) { doSomething(arg) {
expect(arg).toEqual({}); expect(arg).toEqual({});
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1290,7 +1350,7 @@ describe("t-on", () => {
{ {
doSomething(arg) { doSomething(arg) {
expect(arg).toEqual({}); expect(arg).toEqual({});
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1312,7 +1372,7 @@ describe("t-on", () => {
{ {
activate(action) { activate(action) {
expect(action).toBe("someval"); expect(action).toBe("someval");
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1325,7 +1385,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();
@@ -1335,8 +1395,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);
@@ -1349,10 +1409,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);
@@ -1364,8 +1424,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);
@@ -1382,8 +1442,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);
@@ -1399,7 +1459,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: [] });
@@ -1416,7 +1476,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: [] });
@@ -1445,7 +1505,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: [] });
@@ -1471,7 +1531,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: [] });
@@ -1495,10 +1555,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);
}); });
@@ -1520,10 +1580,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);
}); });
@@ -1551,13 +1611,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: [] });
@@ -1582,7 +1642,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);
}); });
@@ -1598,7 +1658,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: [] });
@@ -1617,7 +1677,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: [] });
@@ -1644,7 +1704,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: [] });
@@ -1808,7 +1868,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();
}); });
@@ -1921,9 +1981,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>");
@@ -1931,9 +1991,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",
@@ -1948,9 +2008,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++);
+93 -93
View File
@@ -1,5 +1,5 @@
import { Component, Env } from "../src/component/component"; import { Component, Env } from "../src/component/component";
import { Store, useStore, useDispatch, useGetters } from "../src/store"; import { Store, useStore, useDispatch, useGetters, EnvWithStore } from "../src/store";
import { useState } from "../src/hooks"; import { useState } from "../src/hooks";
import { xml } from "../src/tags"; import { xml } from "../src/tags";
import { shallowEqual } from "../src/utils"; import { shallowEqual } from "../src/utils";
@@ -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,18 +874,18 @@ 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 { class TodoItem extends Component<{}, EnvWithStore> {
static template = xml` static template = xml`
<div class="todo"> <div class="todo">
<t t-esc="state.todo.title"/> <t t-esc="state.todo.title"/>
@@ -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,18 +947,18 @@ 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 { class TodoItem extends Component<{}, EnvWithStore> {
static template = xml` static template = xml`
<div class="todo"> <div class="todo">
<t t-esc="state.todo.title"/> <t t-esc="state.todo.title"/>
@@ -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 -
+5
View File
@@ -1,4 +1,5 @@
import { escape, debounce } from "../src/utils"; import { escape, debounce } from "../src/utils";
import { browser } from "../src/browser";
describe("escape", () => { describe("escape", () => {
test("normal strings", () => { test("normal strings", () => {
@@ -15,6 +16,10 @@ describe("escape", () => {
describe("debounce", () => { describe("debounce", () => {
test("works as expected", () => { test("works as expected", () => {
jest.useFakeTimers(); jest.useFakeTimers();
// need to reset them on browser because they were mocked in window, but not
// on browser object
browser.setTimeout = window.setTimeout.bind(window);
browser.clearTimeout = window.clearTimeout.bind(window);
let n = 0; let n = 0;
let f = debounce(() => n++, 100); let f = debounce(() => n++, 100);
expect(n).toBe(0); expect(n).toBe(0);
+199 -192
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1169,7 +1169,7 @@ const RESPONSIVE_CSS = `body {
const SLOTS = `// We show here how slots can be used to create generic components. const SLOTS = `// We show here how slots can be used to create generic components.
// In this example, the Card component is basically only a container. It is not // In this example, the Card component is basically only a container. It is not
// aware of its content. It just knows where it should be (with t-slot). // aware of its content. It just knows where it should be (with t-slot).
// The parent component define the content with t-set. // The parent component define the content with t-set-slot.
// //
// Note that the t-on-click event, defined in the App template, is executed in // Note that the t-on-click event, defined in the App template, is executed in
// the context of the App component, even though it is inside the Card component // the context of the App component, even though it is inside the Card component
@@ -1235,15 +1235,15 @@ const SLOTS_XML = `<templates>
<div t-name="App" class="main"> <div t-name="App" class="main">
<Card title="'Title card A'"> <Card title="'Title card A'">
<t t-set="content">Content of card 1... [<t t-esc="state.a"/>]</t> <t t-set-slot="content">Content of card 1... [<t t-esc="state.a"/>]</t>
<t t-set="footer"><button t-on-click="inc('a', 1)">Increment A</button></t> <t t-set-slot="footer"><button t-on-click="inc('a', 1)">Increment A</button></t>
</Card> </Card>
<Card title="'Title card B'"> <Card title="'Title card B'">
<t t-set="content"> <t t-set-slot="content">
<div>Card 2... [<t t-esc="state.b"/>]</div> <div>Card 2... [<t t-esc="state.b"/>]</div>
<Counter /> <Counter />
</t> </t>
<t t-set="footer"><button t-on-click="inc('b', -1)">Decrement B</button></t> <t t-set-slot="footer"><button t-on-click="inc('b', -1)">Decrement B</button></t>
</Card> </Card>
</div> </div>
</templates>`; </templates>`;
-1
View File
@@ -106,7 +106,6 @@ async function startRelease() {
draft: true // todo: remove this someday draft: true // todo: remove this someday
}; };
const result = await createRelease(token, options); const result = await createRelease(token, options);
console.log(result);
await ask("Ready for next step..."); await ask("Ready for next step...");
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------