Compare commits

...

40 Commits

Author SHA1 Message Date
Géry Debongnie e73fb462c5 [REL] v1.0.10
# v1.0.10

Bug fixes!

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

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

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

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

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

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

It is then interpreted as:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

<input value="abc"/>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

But the code did not work in the other direction:

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

With this commit, we just add the missing if statement

closes #664
2020-04-21 16:23:31 +02:00
Géry Debongnie 23ce19e57a [REL] v1.0.7 2020-04-17 15:52:52 +02:00
Géry Debongnie aaf40e9bec [IMP] tooling: add iife build to npm package
This is useful to link to the iife version in some other projects.
2020-04-17 15:47:20 +02:00
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
74 changed files with 2504 additions and 961 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:
- [owl-1.0.5.js](https://github.com/odoo/owl/releases/download/v1.0.5/owl.js)
- [owl-1.0.5.min.js](https://github.com/odoo/owl/releases/download/v1.0.5/owl.min.js)
- [owl-1.0.10.js](https://github.com/odoo/owl/releases/download/v1.0.10/owl.js)
- [owl-1.0.10.min.js](https://github.com/odoo/owl/releases/download/v1.0.10/owl.min.js)
## License
+1 -1
View File
@@ -111,7 +111,7 @@ class Parent extends Component {
static components = { OrderLine };
orders = useState([
{ id: 1, name: "Coffee", quantity: 0 },
{ id: 2, name: "Tea", quantity: 0 }
{ id: 2, name: "Tea", quantity: 0 },
]);
addToOrder(event) {
+11 -11
View File
@@ -281,7 +281,7 @@ import { Component } from "@odoo/owl";
import "regenerator-runtime/runtime";
export async function nextTick() {
return new Promise(function(resolve) {
return new Promise(function (resolve) {
setTimeout(() => Component.scheduler.requestAnimationFrame(() => resolve()));
});
}
@@ -361,26 +361,26 @@ const HtmlWebpackPlugin = require("html-webpack-plugin");
const host = process.env.HOST || "localhost";
module.exports = function(env, argv) {
module.exports = function (env, argv) {
const mode = argv.mode || "development";
return {
mode: mode,
entry: "./src/main.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist")
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: "babel-loader",
exclude: /node_modules/
}
]
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".js", ".jsx"]
extensions: [".js", ".jsx"],
},
devServer: {
contentBase: path.resolve(__dirname, "public/index.html"),
@@ -388,14 +388,14 @@ module.exports = function(env, argv) {
hot: true,
host,
port: 3000,
publicPath: "/"
publicPath: "/",
},
plugins: [
new HtmlWebpackPlugin({
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:
```js
(function() {
(function () {
console.log("hello owl", owl.__info__.version);
})();
```
@@ -170,13 +170,13 @@ class App extends Component {
{
id: 1,
title: "buy milk",
isCompleted: true
isCompleted: true,
},
{
id: 2,
title: "clean house",
isCompleted: false
}
isCompleted: false,
},
];
}
```
@@ -562,23 +562,23 @@ const actions = {
const task = {
id: state.nextId++,
title: title,
isCompleted: false
isCompleted: false,
};
state.tasks.push(task);
}
},
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;
},
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);
}
},
};
const initialState = {
nextId: 1,
tasks: []
tasks: [],
};
// -------------------------------------------------------------------------
@@ -616,7 +616,7 @@ class App extends Component {
static components = { Task };
inputRef = useRef("add-input");
tasks = useStore(state => state.tasks);
tasks = useStore((state) => state.tasks);
dispatch = useDispatch();
mounted() {
@@ -811,7 +811,7 @@ For reference, here is the final code:
```
```js
(function() {
(function () {
const { Component, Store } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
@@ -827,24 +827,24 @@ For reference, here is the final code:
const task = {
id: state.nextId++,
title: title,
isCompleted: false
isCompleted: false,
};
state.tasks.push(task);
}
},
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;
},
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);
}
},
};
const initialState = {
nextId: 1,
tasks: []
tasks: [],
};
// -------------------------------------------------------------------------
@@ -897,7 +897,7 @@ For reference, here is the final code:
static components = { Task };
inputRef = useRef("add-input");
tasks = useStore(state => state.tasks);
tasks = useStore((state) => state.tasks);
filter = useState({ value: "all" });
dispatch = useDispatch();
@@ -916,9 +916,9 @@ For reference, here is the final code:
get displayedTasks() {
switch (this.filter.value) {
case "active":
return this.tasks.filter(t => !t.isCompleted);
return this.tasks.filter((t) => !t.isCompleted);
case "completed":
return this.tasks.filter(t => t.isCompleted);
return this.tasks.filter((t) => t.isCompleted);
case "all":
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 = {
increment({ state }, val) {
state.counter.value += val;
}
},
};
const state = {
counter: { value: 0 }
counter: { value: 0 },
};
const store = new owl.Store({ state, actions });
@@ -270,7 +270,7 @@ class Counter extends Component {
<button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="counter.value"/>]
</button>`;
counter = useStore(state => state.counter);
counter = useStore((state) => state.counter);
dispatch = useDispatch();
}
+4 -4
View File
@@ -204,7 +204,7 @@ to be called in the constructor.
class Counter extends owl.Component {
static props = {
initialValue: Number,
optional: true
optional: true,
};
}
```
@@ -217,7 +217,7 @@ to be called in the constructor.
```js
class Counter extends owl.Component {
static defaultProps = {
initialValue: 0
initialValue: 0,
};
}
```
@@ -804,8 +804,8 @@ class RootNode extends Component {
children: [
{ label: "b" },
{ 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)
- [Using a sub environment](#using-a-sub-environment)
- [Content of an Environment](#content-of-an-environment)
- [Special keys](#special-keys)
## Overview
@@ -93,7 +94,7 @@ Some good use cases for additional keys in the environment are:
- some configuration keys,
- 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
create a test environment with mock services.
@@ -112,10 +113,10 @@ async function myEnv() {
qweb: qweb,
services: {
localStorage: localStorage,
rpc: rpc
rpc: rpc,
},
debug: false,
inMobileMode: true
inMobileMode: true,
};
}
@@ -125,3 +126,26 @@ async function start() {
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.
```js
env.qweb.on("error", null, function(error) {
env.qweb.on("error", null, function (error) {
// do something
// react to the error
});
+1 -1
View File
@@ -7,7 +7,7 @@ triggering events, and callbacks.
```js
const bus = new owl.core.EventBus();
bus.on("some-event", null, function(...args) {
bus.on("some-event", null, function (...args) {
console.log(...args);
});
+1 -1
View File
@@ -259,7 +259,7 @@ function useLoader() {
}
onWillStart(() => updateRecord(component.props.id));
onWillUpdateProps(nextProps => updateRecord(nextProps.id));
onWillUpdateProps((nextProps) => updateRecord(nextProps.id));
return record;
}
+1 -1
View File
@@ -30,7 +30,7 @@ class ComponentB extends owl.Component {
count: {type: Number},
messages: {
type: Array,
element: {type: Object, shape: {id: Boolean, text: 'string' }
element: {type: Object, shape: {id: Boolean, text: String }
},
date: Date,
combinedVal: [Number, Boolean]
+2 -2
View File
@@ -114,9 +114,9 @@ For example:
const translations = {
hello: "bonjour",
yes: "oui",
no: "non"
no: "non",
};
const translateFn = str => translations[str] || str;
const translateFn = (str) => translations[str] || str;
const qweb = new QWeb({ translateFn });
```
+1 -1
View File
@@ -532,7 +532,7 @@ The javascript QWeb implementation provides two useful debugging directives:
```xml
<t t-if="a_test">
<t t-debug="">
<t t-debug=""/>
</t>
```
+6 -3
View File
@@ -37,16 +37,16 @@ rendering context.
</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
<div t-name="SomeComponent">
<div>some component</div>
<Dialog title="Some Dialog">
<t t-set="content">
<t t-set-slot="content">
<div>hey</div>
</t>
<t t-set="footer">
<t t-set-slot="footer">
<button t-on-click="doSomething">ok</button>
</t>
</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
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
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({
id: state.nextId++,
message,
isCompleted: false
isCompleted: false,
});
}
},
};
const state = {
todos: [],
nextId: 1
nextId: 1,
};
const store = new owl.Store({ state, actions });
@@ -90,7 +90,7 @@ const config = {
state,
actions,
getters,
env
env,
};
const store = new Store(config);
```
@@ -110,7 +110,7 @@ const actions = {
} catch (e) {
state.loginState = "error";
}
}
},
};
```
@@ -144,7 +144,7 @@ const actions = {
state.recordId = recordId;
const data = await doSomeRPC("/read/", recordId);
state.recordData = data;
}
},
};
```
@@ -159,7 +159,7 @@ const actions = {
const data = await doSomeRPC("/read/", recordId);
state.recordId = recordId;
state.recordData = data;
}
},
};
```
@@ -187,14 +187,14 @@ transform the data contained in the store.
```js
const getters = {
getPost({ state }, id) {
const post = state.posts.find(p => p.id === id);
const author = state.authors.find(a => a.id === post.id);
const post = state.posts.find((p) => p.id === id);
const author = state.authors.find((a) => a.id === post.id);
return {
id,
author,
content: post.content
content: post.content,
};
}
},
};
// somewhere else
@@ -224,11 +224,11 @@ Assume we have this store:
const actions = {
increment({ state }, val) {
state.counter.value += val;
}
},
};
const state = {
counter: { value: 0 }
counter: { value: 0 },
};
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
class Counter extends Component {
counter = useStore(state => state.counter);
counter = useStore((state) => state.counter);
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).
```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 app = new App({ qweb });
app.mount(document.body);
@@ -29,7 +29,7 @@ Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function([t
or alternatively:
```js
owl.utils.whenReady(function() {
owl.utils.whenReady(function () {
const qweb = new owl.QWeb();
const app = new App({ qweb });
app.mount(document.body);
+7 -5
View File
@@ -1,12 +1,13 @@
{
"name": "@odoo/owl",
"version": "1.0.5",
"version": "1.0.10",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.js",
"types": "dist/types/index.d.ts",
"files": [
"dist/types/",
"dist/owl.js"
"dist/owl.js",
"dist/owl-iife.js"
],
"engines": {
"node": ">=10.15.3"
@@ -24,7 +25,7 @@
"pretools:watch": "npm run build",
"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",
"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"
},
"repository": {
@@ -48,7 +49,7 @@
"jest-environment-jsdom": "^24.7.1",
"live-server": "^1.2.1",
"npm-run-all": "^4.1.5",
"prettier": "^1.19.1",
"prettier": "^2.0.4",
"rollup": "^1.6.0",
"rollup-plugin-typescript2": "^0.20.1",
"sass": "^1.16.1",
@@ -77,6 +78,7 @@
]
},
"prettier": {
"printWidth": 100
"printWidth": 100,
"endOfLine": "auto"
}
}
+3 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.0.5
- Current version: 1.0.10
- Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may
@@ -16,6 +16,8 @@ change!
### 2.x (2020? 2021? 2022?)
- stop support for `t-set` directive to define the content of a slot
Maybe:
- 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,
};
+44 -16
View File
@@ -7,6 +7,7 @@ import { Fiber } from "./fiber";
import "./props_validation";
import { Scheduler, scheduler } from "./scheduler";
import { activateSheet } from "./styles";
import { Browser, browser } from "../browser";
/**
* Owl Component System
@@ -34,7 +35,7 @@ import { activateSheet } from "./styles";
*/
export interface Env {
qweb: QWeb;
[key: string]: any;
browser: Browser;
}
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) {
this.env.qweb = new QWeb();
}
if (!this.env.browser) {
this.env.browser = browser;
}
this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) {
this.render(true);
@@ -199,7 +203,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
renderFn: qweb.render.bind(qweb, template),
classObj: null,
refs: null,
scope: null
scope: null,
};
if (constr.style) {
this.__applyStyles(constr);
@@ -312,6 +316,9 @@ export class Component<Props extends {} = any, T extends Env = Env> {
return Promise.resolve();
}
}
if (__owl__.isDestroyed) {
throw new Error("Cannot mount a destroyed component");
}
if (__owl__.currentFiber) {
const currentFiber = __owl__.currentFiber;
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> {
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,
// or was unmounted and some state change triggered a render. Either way,
// we do not want to actually render anything in this case.
return;
}
if (__owl__.currentFiber && !__owl__.currentFiber.isRendered) {
return scheduler.addFiber(__owl__.currentFiber.root);
if (currentFiber && !currentFiber.isRendered && !currentFiber.isCompleted) {
return scheduler.addFiber(currentFiber.root);
}
// 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
@@ -488,9 +496,9 @@ export class Component<Props extends {} = any, T extends Env = Env> {
}
this.willUnmount();
__owl__.isMounted = false;
if (this.__owl__.currentFiber) {
this.__owl__.currentFiber.isCompleted = true;
this.__owl__.currentFiber.root.counter = 0;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true;
__owl__.currentFiber.root.counter = 0;
}
const children = __owl__.children;
for (let id in children) {
@@ -509,7 +517,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
const ev = new OwlEvent(component, eventType, {
bubbles: true,
cancelable: true,
detail: payload
detail: payload,
});
const triggerHook = this.env[portalSymbol as any];
if (triggerHook) {
@@ -544,7 +552,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
}
await Promise.all([
this.willUpdateProps(nextProps),
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps)
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps),
]);
if (fiber.isCompleted) {
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
// one of its subclass).
let template: string;
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
let template: string = p.name;
while (!(template in qweb.templates) && p !== Component) {
p = p.__proto__;
template = p.name;
}
if (p === Component) {
throw new Error(`Could not find template for component "${this.constructor.name}"`);
@@ -640,16 +649,35 @@ export class Component<Props extends {} = any, T extends Env = Env> {
try {
let vnode = __owl__.renderFn!(this, {
handlers: __owl__.boundHandlers,
fiber: fiber
fiber: fiber,
});
// 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
// destroyed right now, because they are not in the DOM, and thus we won't
// be notified later on (when patching), that they are removed from the DOM
for (let childKey in __owl__.children) {
let child = __owl__.children[childKey];
if (!child.__owl__.isMounted && child.__owl__.parentLastFiberId < fiber.id) {
child.destroy();
const child = __owl__.children[childKey];
const childOwl = child.__owl__;
if (!childOwl.isMounted && childOwl.parentLastFiberId < fiber.id) {
// we only do here a "soft" destroy, meaning that we leave the child
// dom node alone, without removing it. Most of the time, it does not
// matter, because the child component is already unmounted. However,
// if some of its parent have been unmounted, the child could actually
// still be attached to its parent, and this may be important if we
// want to remount the parent, because the vdom need to match the
// actual DOM
child.__destroy(childOwl.parent);
if (childOwl.pvnode) {
// we remove the key here to make sure that the patching algorithm
// is able to make the difference between this pvnode and an eventual
// other instance of the same component
delete childOwl.pvnode.key;
// Since the component has been unmounted, we do not want to actually
// call a remove hook. This is pretty important, since the t-component
// directive actually disabled it, so the vdom algorithm will just
// not remove the child elm if we don't remove the hook.
delete childOwl.pvnode.data!.hook!.remove;
}
}
}
if (!vnode) {
+30 -9
View File
@@ -7,7 +7,7 @@ import { makeHandlerCode, MODS_CODE } from "../qweb/extensions";
//------------------------------------------------------------------------------
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) {
@@ -18,7 +18,7 @@ QWeb.utils.defineProxy = function defineProxy(target, source) {
},
set(val) {
source[k] = val;
}
},
});
}
};
@@ -238,7 +238,7 @@ QWeb.addDirective({
// computing the props string representing the props object
let propStr = Object.keys(props)
.map(k => k + ":" + props[k])
.map((k) => k + ":" + props[k])
.join(",");
let componentID = ctx.generateID();
@@ -280,7 +280,7 @@ QWeb.addDirective({
let classDef = classAttr
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.map((a) => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
@@ -298,7 +298,7 @@ QWeb.addDirective({
}
}
let eventsCode = events
.map(function([name, value]) {
.map(function ([name, value]) {
const capture = name.match(/\.capture/);
name = capture ? name.replace(/\.capture/, "") : name;
const { event, handler } = makeHandlerCode(
@@ -400,15 +400,36 @@ QWeb.addDirective({
if (hasSlots) {
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++;
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
if (slotNodes.length) {
for (let i = 0, length = slotNodes.length; i < length; i++) {
const slotNode = slotNodes[i];
slotNode.parentElement!.removeChild(slotNode);
const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set");
let key = slotNode.getAttribute("t-set-slot")!;
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);
QWeb.slots[`${slotId}_${key}`] = slotFn;
}
@@ -448,5 +469,5 @@ QWeb.addDirective({
ctx.addLine(`w${componentID}.__owl__.parentLastFiberId = extra.fiber.id;`);
return true;
}
},
});
+17 -3
View File
@@ -193,7 +193,7 @@ export class Fiber {
// build patchQueue
const patchQueue: Fiber[] = [];
const doWork: (Fiber) => Fiber | null = function(f) {
const doWork: (Fiber) => Fiber | null = function (f) {
patchQueue.push(f);
return f.child;
};
@@ -225,6 +225,12 @@ export class Fiber {
`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 {
target = component.__owl__.vnode || document.createElement(fiber.vnode!.sel!);
}
@@ -232,12 +238,20 @@ export class Fiber {
} else {
if (fiber.shouldPatch) {
component.__patch(component.__owl__.vnode!, fiber.vnode!);
// When updating a Component's props (in directive),
// the component has a pvnode AND should be patched.
// However, its pvnode.elm may have changed if it is a High Order Component
if (component.__owl__.pvnode) {
component.__owl__.pvnode.elm = component.__owl__.vnode!.elm;
}
} else {
component.__patch(document.createElement(fiber.vnode!.sel!), fiber.vnode!);
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
}
}
component.__owl__.currentFiber = null;
if (fiber === component.__owl__.currentFiber) {
component.__owl__.currentFiber = null;
}
}
// insert into the DOM (mount case)
@@ -274,7 +288,7 @@ export class Fiber {
* Cancel a fiber and all its children.
*/
cancel() {
this._walk(f => {
this._walk((f) => {
if (!f.isRendered) {
f.root.counter--;
}
+2 -2
View File
@@ -11,7 +11,7 @@ import { QWeb } from "../qweb/index";
* This is why it is only done in 'dev' mode.
*/
QWeb.utils.validateProps = function(Widget, props: Object) {
QWeb.utils.validateProps = function (Widget, props: Object) {
const propsDef = (<any>Widget).props;
if (propsDef instanceof Array) {
// list of strings (prop names)
@@ -37,7 +37,7 @@ QWeb.utils.validateProps = function(Widget, props: Object) {
if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(`Missing props '${propName}' (component '${Widget.name}')`);
} else {
break;
continue;
}
}
let isValid;
+7 -7
View File
@@ -1,4 +1,5 @@
import { Fiber } from "./fiber";
import { browser } from "../browser";
/**
* Owl Scheduler Class
@@ -19,9 +20,9 @@ interface Task {
export class Scheduler {
tasks: Task[] = [];
isRunning: boolean = false;
requestAnimationFrame: typeof window.requestAnimationFrame;
requestAnimationFrame: Window["requestAnimationFrame"];
constructor(requestAnimationFrame) {
constructor(requestAnimationFrame: Window["requestAnimationFrame"]) {
this.requestAnimationFrame = requestAnimationFrame;
}
@@ -49,7 +50,7 @@ export class Scheduler {
return reject(fiber.error);
}
resolve();
}
},
});
if (!this.isRunning) {
this.start();
@@ -59,7 +60,7 @@ export class Scheduler {
rejectFiber(fiber: Fiber, reason: string) {
fiber = fiber.root;
const index = this.tasks.findIndex(t => t.fiber === fiber);
const index = this.tasks.findIndex((t) => t.fiber === fiber);
if (index >= 0) {
const [task] = this.tasks.splice(index, 1);
fiber.cancel();
@@ -75,7 +76,7 @@ export class Scheduler {
flush() {
let tasks = this.tasks;
this.tasks = [];
tasks = tasks.filter(task => {
tasks = tasks.filter((task) => {
if (task.fiber.isCompleted) {
task.callback();
return false;
@@ -109,5 +110,4 @@ export class Scheduler {
}
}
const raf = window.requestAnimationFrame.bind(window);
export const scheduler = new Scheduler(raf);
export const scheduler = new Scheduler(browser.requestAnimationFrame);
+1 -1
View File
@@ -8,7 +8,7 @@
export const STYLESHEETS: { [id: string]: HTMLStyleElement } = {};
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 parts: string[] = [];
let rules: string[] = [];
+1 -1
View File
@@ -26,5 +26,5 @@ Object.defineProperty(config, "mode", {
} else {
console.log(`Owl is now running in 'prod' mode.`);
}
}
},
});
+6 -6
View File
@@ -79,9 +79,9 @@ export class Context extends EventBus {
async __notifyComponents() {
const rev = ++this.rev;
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) {
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
// 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
@@ -116,7 +116,7 @@ export function useContextWithCB(ctx: Context, component: Component, method): an
__owl__.observer.notifyCB = component.render.bind(component);
}
const currentCB = __owl__.observer.notifyCB;
__owl__.observer.notifyCB = function() {
__owl__.observer.notifyCB = function () {
if (ctx.rev > mapping[id]) {
// 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
@@ -128,18 +128,18 @@ export function useContextWithCB(ctx: Context, component: Component, method): an
mapping[id] = 0;
const renderFn = __owl__.renderFn;
__owl__.renderFn = function(comp, params) {
__owl__.renderFn = function (comp, params) {
mapping[id] = ctx.rev;
return renderFn(comp, params);
};
ctx.on("update", component, async contextRev => {
ctx.on("update", component, async (contextRev) => {
if (mapping[id] < contextRev) {
mapping[id] = contextRev;
await method();
}
});
const __destroy = component.__destroy;
component.__destroy = parent => {
component.__destroy = (parent) => {
ctx.off("update", component);
delete mapping[id];
__destroy.call(component, parent);
+2 -2
View File
@@ -44,7 +44,7 @@ export class EventBus {
}
this.subscriptions[eventType].push({
owner,
callback
callback,
});
}
@@ -54,7 +54,7 @@ export class EventBus {
off(eventType: string, owner: any) {
const subs = this.subscriptions[eventType];
if (subs) {
this.subscriptions[eventType] = subs.filter(s => s.owner !== owner);
this.subscriptions[eventType] = subs.filter((s) => s.owner !== owner);
}
}
+8 -3
View File
@@ -25,7 +25,12 @@ export class Observer {
notifyCB() {}
observe<T>(value: T, parent?: any): T {
if (value === null || typeof value !== "object" || value instanceof Date) {
if (
value === null ||
typeof value !== "object" ||
value instanceof Date ||
value instanceof Promise
) {
// fun fact: typeof null === 'object'
return value;
}
@@ -67,14 +72,14 @@ export class Observer {
self.notifyCB();
}
return true;
}
},
});
const metadata = {
value,
proxy,
rev: this.rev,
parent
parent,
};
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) {
if (reverse) {
return function(cb) {
return function (cb) {
const component: Component = Component.current!;
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function() {
component.__owl__[method] = function () {
current.call(component);
cb.call(component);
};
@@ -49,11 +49,11 @@ function makeLifecycleHook(method: string, reverse: boolean = false) {
}
};
} else {
return function(cb) {
return function (cb) {
const component: Component = Component.current!;
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function() {
component.__owl__[method] = function () {
cb.call(component);
current.call(component);
};
@@ -65,12 +65,12 @@ function makeLifecycleHook(method: string, reverse: boolean = false) {
}
function makeAsyncHook(method: string) {
return function(cb) {
return function (cb) {
const component: Component = Component.current!;
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function(...args) {
return Promise.all[(current.call(component, ...args), cb.call(component, ...args))];
component.__owl__[method] = function (...args) {
return Promise.all([current.call(component, ...args), cb.call(component, ...args)]);
};
} else {
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
* html node or component.
*/
interface Ref {
interface Ref<C extends Component = Component> {
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__;
return {
get el(): HTMLElement | null {
@@ -111,10 +111,10 @@ export function useRef(name: string): Ref {
}
return null;
},
get comp(): Component | null {
get comp(): C | null {
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,
useDispatch: _store.useDispatch,
useGetters: _store.useGetters,
useStore: _store.useStore
useStore: _store.useStore,
});
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 props = {
target: {
type: String
}
type: String,
},
};
// 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
// register an handler to those events just before children will trigger them
useSubEnv({
[portalSymbol]: ev => {
[portalSymbol]: (ev) => {
if (!this._handledEvents.has(ev.type)) {
this.portal!.elm!.addEventListener(ev.type, this._handlerTunnel);
this._handledEvents.add(ev.type);
}
}
},
});
}
/**
+20 -22
View File
@@ -49,6 +49,7 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
if (ctx.escaping) {
let protectID;
if (value.hasBody) {
ctx.rootContext.shouldDefineUtils = true;
protectID = ctx.startProtectScope();
ctx.addLine(
`${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};`
@@ -96,7 +97,7 @@ QWeb.addDirective({
let value = ctx.getValue(node.getAttribute("t-esc")!);
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
return true;
}
},
});
QWeb.addDirective({
@@ -106,7 +107,7 @@ QWeb.addDirective({
let value = ctx.getValue(node.getAttribute("t-raw")!);
compileValueNode(value, node, qweb, ctx);
return true;
}
},
});
//------------------------------------------------------------------------------
@@ -163,7 +164,7 @@ QWeb.addDirective({
}
}
return true;
}
},
});
//------------------------------------------------------------------------------
@@ -179,7 +180,7 @@ QWeb.addDirective({
},
finalize({ ctx }) {
ctx.closeIf();
}
},
});
QWeb.addDirective({
@@ -195,7 +196,7 @@ QWeb.addDirective({
},
finalize({ ctx }) {
ctx.closeIf();
}
},
});
QWeb.addDirective({
@@ -208,7 +209,7 @@ QWeb.addDirective({
},
finalize({ ctx }) {
ctx.closeIf();
}
},
});
//------------------------------------------------------------------------------
@@ -222,9 +223,6 @@ QWeb.addDirective({
// ------------------------------------------------
ctx.rootContext.shouldDefineScope = true;
ctx.rootContext.shouldDefineUtils = true;
if (node.nodeName !== "t") {
throw new Error("Invalid tag for t-call directive (should be 't')");
}
const subTemplate = node.getAttribute("t-call")!;
const nodeTemplate = qweb.templates[subTemplate];
if (!nodeTemplate) {
@@ -233,21 +231,22 @@ QWeb.addDirective({
// Step 2: compile target template in sub templates
// ------------------------------------------------
if (!qweb.subTemplates[subTemplate]) {
qweb.subTemplates[subTemplate] = true;
let subId = qweb.subTemplates[subTemplate];
if (!subId) {
subId = QWeb.nextId++;
qweb.subTemplates[subTemplate] = subId;
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
// ------------------------------------------------
let hasBody = node.hasChildNodes();
let protectID;
const protectID = ctx.startProtectScope();
if (hasBody) {
// we add a sub scope to protect the ambient scope
ctx.addLine(`{`);
ctx.indent();
protectID = ctx.startProtectScope();
const nodeCopy = node.cloneNode(true) as Element;
for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) {
nodeCopy.removeAttribute(attr);
@@ -268,31 +267,30 @@ QWeb.addDirective({
// Step 4: add the appropriate function call to current component
// ------------------------------------------------
const callingScope = hasBody ? "scope" : "Object.assign(Object.create(context), scope)";
const parentComponent = `utils.getComponent(context)`;
const key = ctx.generateTemplateKey();
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`;
if (ctx.parentNode) {
ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, ${extra});`);
ctx.addLine(`this.constructor.subTemplates['${subId}'].call(this, scope, ${extra});`);
} else {
// this is a t-call with no parentnode, we need to extract the result
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`result = []`);
ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, ${extra});`);
ctx.addLine(`this.constructor.subTemplates['${subId}'].call(this, scope, ${extra});`);
ctx.addLine(`result = result[0]`);
}
// Step 5: restore previous scope
// ------------------------------------------------
if (hasBody) {
ctx.stopProtectScope(protectID);
ctx.dedent();
ctx.addLine(`}`);
}
ctx.stopProtectScope(protectID);
return true;
}
},
});
//------------------------------------------------------------------------------
@@ -353,7 +351,7 @@ QWeb.addDirective({
ctx.addLine("}");
ctx.stopProtectScope(varsID);
return true;
}
},
});
//------------------------------------------------------------------------------
@@ -364,7 +362,7 @@ QWeb.addDirective({
priority: 1,
atNodeEncounter({ ctx }) {
ctx.addLine("debugger;");
}
},
});
//------------------------------------------------------------------------------
@@ -376,5 +374,5 @@ QWeb.addDirective({
atNodeEncounter({ ctx, value }) {
const expr = ctx.formatExpression(value);
ctx.addLine(`console.log(${expr})`);
}
},
});
+6 -5
View File
@@ -162,7 +162,7 @@ export class CompilationContext {
const tokens = compileExprToArray(expr, this.variables);
const done = new Set();
return tokens
.map(tok => {
.map((tok) => {
if (tok.varName) {
if (!done.has(tok.varName)) {
done.add(tok.varName);
@@ -189,18 +189,19 @@ export class CompilationContext {
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 + "`";
}
startProtectScope(codeBlock?: boolean): number {
const protectID = this.generateID();
this.rootContext.protectedScopeNumber++;
this.rootContext.shouldDefineScope = true;
const scopeExpr = codeBlock
? `Object.create(scope);`
: `Object.assign(Object.create(context), scope);`;
const scopeExpr = `Object.create(scope);`;
this.addLine(`let _origScope${protectID} = scope;`);
this.addLine(`scope = ${scopeExpr}`);
if (!codeBlock) {
this.addLine(`scope.__access_mode__ = 'ro';`);
}
return protectID;
}
stopProtectScope(protectID: number) {
+9 -9
View File
@@ -35,7 +35,7 @@ const WORD_REPLACEMENT = {
gt: ">",
gte: ">=",
lt: "<",
lte: "<="
lte: "<=",
};
export interface QWebVar {
@@ -77,7 +77,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
":": "COLON",
",": "COMMA",
"(": "LEFT_PAREN",
")": "RIGHT_PAREN"
")": "RIGHT_PAREN",
};
// 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;
let tokenizeString: Tokenizer = function(expr) {
let tokenizeString: Tokenizer = function (expr) {
let s = expr[0];
let start = s;
if (s !== "'" && s !== '"') {
@@ -114,7 +114,7 @@ let tokenizeString: Tokenizer = function(expr) {
return { type: "VALUE", value: s };
};
let tokenizeNumber: Tokenizer = function(expr) {
let tokenizeNumber: Tokenizer = function (expr) {
let s = expr[0];
if (s && s.match(/[0-9]/)) {
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];
if (s && s.match(/[a-zA-Z_\$]/)) {
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];
if (char && char in STATIC_TOKEN_MAP) {
return { type: STATIC_TOKEN_MAP[char], value: char };
@@ -153,7 +153,7 @@ const tokenizeStatic: Tokenizer = function(expr) {
return false;
};
const tokenizeOperator: Tokenizer = function(expr) {
const tokenizeOperator: Tokenizer = function (expr) {
for (let op of OPERATORS) {
if (expr.startsWith(op)) {
return { type: "OPERATOR", value: op };
@@ -167,7 +167,7 @@ const TOKENIZERS = [
tokenizeNumber,
tokenizeOperator,
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 {
return compileExprToArray(expr, scope)
.map(t => t.value)
.map((t) => t.value)
.join("");
}
+51 -22
View File
@@ -23,7 +23,7 @@ import { QWeb } from "./qweb";
export const MODS_CODE = {
prevent: "e.preventDefault();",
self: "if (e.target !== this.elm) {return}",
stop: "e.stopPropagation();"
stop: "e.stopPropagation();",
};
interface HandlerInfo {
@@ -50,7 +50,7 @@ export function makeHandlerCode(
let code: string;
// check if it is a method with no args, a method with args or an expression
let args: string = "";
const name: string = value.replace(/\(.*\)/, function(_args) {
const name: string = value.replace(/\(.*\)/, function (_args) {
args = _args.slice(1, -1);
return "";
});
@@ -74,7 +74,7 @@ export function makeHandlerCode(
putInCache = false;
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}}`;
if (putInCache) {
const key = ctx.generateTemplateKey(event);
@@ -90,7 +90,7 @@ QWeb.addDirective({
atNodeCreation({ ctx, fullName, value, nodeID }) {
const { event, handler } = makeHandlerCode(ctx, fullName, value, true);
ctx.addLine(`p${nodeID}.on['${event}'] = ${handler};`);
}
},
});
//------------------------------------------------------------------------------
@@ -105,17 +105,17 @@ QWeb.addDirective({
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`);
}
},
});
//------------------------------------------------------------------------------
// t-transition
//------------------------------------------------------------------------------
QWeb.utils.nextFrame = function(cb: () => void) {
QWeb.utils.nextFrame = function (cb: () => void) {
requestAnimationFrame(() => requestAnimationFrame(cb));
};
QWeb.utils.transitionInsert = function(vn: VNode, name: string) {
QWeb.utils.transitionInsert = function (vn: VNode, name: string) {
const elm = <HTMLElement>vn.elm;
// remove potential duplicated vnode that is currently being removed, to
// 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;
elm.setAttribute("data-owl-key", vn.key!);
@@ -209,12 +209,12 @@ QWeb.addDirective({
let name = value;
const hooks = {
insert: `utils.transitionInsert(vn, '${name}');`,
remove: `utils.transitionRemove(vn, '${name}', rm);`
remove: `utils.transitionRemove(vn, '${name}', rm);`,
};
for (let hookName in hooks) {
addNodeHook(hookName, hooks[hookName]);
}
}
},
});
//------------------------------------------------------------------------------
@@ -251,17 +251,20 @@ QWeb.addDirective({
}
ctx.closeIf();
return true;
}
},
});
//------------------------------------------------------------------------------
// t-model
//------------------------------------------------------------------------------
QWeb.utils.toNumber = function(val: string): number | string {
QWeb.utils.toNumber = function (val: string): number | string {
const n = parseFloat(val);
return isNaN(n) ? val : n;
};
const hasDotAtTheEnd = /\.[\w_]+\s*$/;
const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
QWeb.addDirective({
name: "model",
priority: 42,
@@ -270,15 +273,41 @@ QWeb.addDirective({
let handler;
let event = fullName.includes(".lazy") ? "change" : "input";
// we keep here a reference to the "base expression" (if the expression
// is `t-model="some.expr.value", then the base expression is "some.expr").
// This is necessary so we can capture it in the handler closure.
let expr = ctx.formatExpression(value);
const index = expr.lastIndexOf(".");
const baseExpr = expr.slice(0, index);
ctx.addLine(`let expr${nodeID} = ${baseExpr};`);
// First step: we need to understand the structure of the expression, and
// from it, extract a base expression (that we can capture, which is
// important because it will be used in a handler later) and a formatted
// expression (which uses the captured base expression)
//
// Also, we support 2 kinds of values: some.expr.value or some.expr[value]
// For the first one, we have:
// - base expression = scope[some].expr
// - expression = exprX.value (where exprX is the var that captures the base expr)
// and for the expression with brackets:
// - base expression = scope[some].expr
// - expression = exprX[keyX] (where exprX is the var that captures the base expr
// and keyX captures scope[value])
let expr: string;
let baseExpr: string;
if (hasDotAtTheEnd.test(value)) {
// we manage the case where the expr has a dot: some.expr.value
const index = value.lastIndexOf(".");
baseExpr = value.slice(0, index);
ctx.addLine(`let expr${nodeID} = ${ctx.formatExpression(baseExpr)};`);
expr = `expr${nodeID}${value.slice(index)}`;
} else if (hasBracketsAtTheEnd.test(value)) {
// we manage here the case where the expr ends in a bracket expression:
// some.expr[value]
const index = value.lastIndexOf("[");
baseExpr = value.slice(0, index);
ctx.addLine(`let expr${nodeID} = ${ctx.formatExpression(baseExpr)};`);
let exprKey = value.trimRight().slice(index + 1, -1);
ctx.addLine(`let exprKey${nodeID} = ${ctx.formatExpression(exprKey)};`);
expr = `expr${nodeID}[exprKey${nodeID}]`;
} else {
throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
}
expr = `expr${nodeID}.${expr.slice(index + 1)}`;
const key = ctx.generateTemplateKey();
if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
@@ -305,7 +334,7 @@ QWeb.addDirective({
}
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`);
}
},
});
//------------------------------------------------------------------------------
@@ -329,5 +358,5 @@ QWeb.addDirective({
if (ctx.loopNumber === 0) {
ctx.rootContext.hasKey0 = ctx.keyStack.pop() as boolean;
}
}
},
});
+88 -56
View File
@@ -66,8 +66,6 @@ interface QWebConfig {
// Const/global stuff/helpers
//------------------------------------------------------------------------------
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const lineBreakRE = /[\r\n]/;
@@ -77,7 +75,7 @@ const NODE_HOOKS_PARAMS = {
create: "(_, n)",
insert: "vn",
remove: "(vn, rm)",
destroy: "()"
destroy: "()",
};
interface Utils {
@@ -86,10 +84,30 @@ interface Utils {
[key: string]: any;
}
function isComponent(obj) {
function isComponent(obj): boolean {
return obj && obj.hasOwnProperty("__owl__");
}
class VDomArray extends Array {
toString() {
return vDomToString(this);
}
}
function vDomToString(vdom: VNode[]): string {
return vdom
.map((vnode) => {
if (vnode.sel) {
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
} else {
return vnode.text;
}
})
.join("");
}
const UTILS: Utils = {
zero: Symbol("zero"),
toObj(expr) {
@@ -111,20 +129,8 @@ const UTILS: Utils = {
addNameSpace(vnode) {
addNS(vnode.data, vnode.children, vnode.sel);
},
VDomArray: class VDomArray extends Array {},
vDomToString: function(vdom: VNode[]): string {
return vdom
.map(vnode => {
if (vnode.sel) {
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
} else {
return vnode.text;
}
})
.join("");
},
VDomArray,
vDomToString,
getComponent(obj) {
while (obj && !isComponent(obj)) {
obj = obj.__proto__;
@@ -133,7 +139,11 @@ const UTILS: Utils = {
},
getScope(obj, property: string) {
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__;
if (!newObj || isComponent(newObj)) {
return obj0;
@@ -141,7 +151,7 @@ const UTILS: Utils = {
obj = newObj;
}
return obj;
}
},
};
function parseXML(xml: string): Document {
@@ -191,7 +201,7 @@ export class QWeb extends EventBus {
name: 1,
att: 1,
attf: 1,
translation: 1
translation: 1,
};
static DIRECTIVES: Directive[] = [];
@@ -208,10 +218,12 @@ export class QWeb extends EventBus {
static slots = {};
static nextSlotId = 1;
// recursiveTemplates contains sub templates called with t-call, but which
// ends up in recursive situations. This is very similar to the slot situation,
// as in we need to propagate the scope.
subTemplates = {};
// subTemplates are stored in two objects: a (local) mapping from a name to an
// id, and a (global) mapping from an id to the compiled function. This is
// necessary to ensure that global templates can be called with more than one
// QWeb instance.
subTemplates: { [key: string]: number } = {};
static subTemplates: { [id: number]: Function } = {};
isUpdating: boolean = false;
translateFn?: QWebConfig["translateFn"];
@@ -235,7 +247,7 @@ export class QWeb extends EventBus {
QWeb.DIRECTIVE_NAMES[directive.name] = 1;
QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
if (directive.extraNames) {
directive.extraNames.forEach(n => (QWeb.DIRECTIVE_NAMES[n] = 1));
directive.extraNames.forEach((n) => (QWeb.DIRECTIVE_NAMES[n] = 1));
}
}
@@ -299,11 +311,11 @@ export class QWeb extends EventBus {
this._processTemplate(elem);
const template = {
elem,
fn: function(this: QWeb, context, extra) {
fn: function (this: QWeb, context, extra) {
const compiledFunction = this._compile(name, elem);
template.fn = compiledFunction;
return compiledFunction.call(this, context, extra);
}
},
};
this.templates[name] = template;
}
@@ -313,10 +325,10 @@ export class QWeb extends EventBus {
for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
let node = tbranch[i];
let prevElem = node.previousElementSibling!;
let pattr = function(name) {
let pattr = function (name) {
return prevElem.getAttribute(name);
};
let nattr = function(name) {
let nattr = function (name) {
return +!!node.getAttribute(name);
};
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
@@ -326,7 +338,7 @@ export class QWeb extends EventBus {
);
}
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;
}) > 1
) {
@@ -503,10 +515,17 @@ export class QWeb extends EventBus {
return;
}
if (node.tagName !== "t" && node.hasAttribute("t-call")) {
const tCallNode = document.createElement("t");
tCallNode.setAttribute("t-call", node.getAttribute("t-call")!);
node.removeAttribute("t-call");
node.prepend(tCallNode);
}
const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) {
// this is a component, we modify in place the xml document to change
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
// <SomeComponent ... /> to <SomeComponent t-component="SomeComponent" ... />
node.setAttribute("t-component", node.tagName);
} else if (node.tagName !== "t" && node.hasAttribute("t-component")) {
throw new Error(
@@ -578,7 +597,7 @@ export class QWeb extends EventBus {
qweb: this,
ctx,
fullName,
value
value,
});
if (isDone) {
for (let { directive, value, fullName } of finalizers) {
@@ -593,7 +612,7 @@ export class QWeb extends EventBus {
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID);
let nodeHooks = {};
let addNodeHook = function(hook, handler) {
let addNodeHook = function (hook, handler) {
nodeHooks[hook] = nodeHooks[hook] || [];
nodeHooks[hook].push(handler);
};
@@ -607,7 +626,7 @@ export class QWeb extends EventBus {
fullName,
value,
nodeID,
addNodeHook
addNodeHook,
});
}
}
@@ -660,22 +679,31 @@ export class QWeb extends EventBus {
const props: string[] = [];
const tattrs: number[] = [];
function handleBooleanProps(key, val) {
function handleProperties(key, val) {
let isProp = false;
if (node.nodeName === "input" && key === "checked") {
let type = (<Element>node).getAttribute("type");
if (type === "checkbox" || type === "radio") {
isProp = true;
}
}
if (node.nodeName === "option" && key === "selected") {
isProp = true;
}
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
isProp = true;
}
if ((key === "readonly" && node.nodeName === "input") || node.nodeName === "textarea") {
isProp = true;
switch (node.nodeName) {
case "input":
let type = (<Element>node).getAttribute("type");
if (type === "checkbox" || type === "radio") {
if (key === "checked" || key === "indeterminate") {
isProp = true;
}
}
if (key === "value" || key === "readonly" || key === "disabled") {
isProp = true;
}
break;
case "option":
isProp = key === "selected" || key === "disabled";
break;
case "textarea":
isProp = key === "readonly" || key === "disabled";
break;
case "button":
case "select":
case "optgroup":
isProp = key === "disabled";
break;
}
if (isProp) {
props.push(`${key}: _${val}`);
@@ -698,10 +726,14 @@ export class QWeb extends EventBus {
if ((value = value.trim())) {
let classDef = value
.split(/\s+/)
.map(a => `'${escapeQuotes(a)}':true`)
.map((a) => `'${escapeQuotes(a)}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
if (classObj) {
ctx.addLine(`Object.assign(${classObj}, {${classDef}})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
}
} else {
ctx.addLine(`let _${attID} = '${escapeQuotes(value)}';`);
@@ -710,7 +742,7 @@ export class QWeb extends EventBus {
name = '"' + name + '"';
}
attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID);
handleProperties(name, attID);
}
}
@@ -742,12 +774,12 @@ export class QWeb extends EventBus {
const attValueID = ctx.generateID();
ctx.addLine(`let _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
const attrIndex = attrs.findIndex((att) => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1);
}
ctx.addLine(`let _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
handleProperties(attName, attID);
}
}
+2 -1
View File
@@ -1,7 +1,8 @@
import { Component } from "../component/component";
import { xml } from "../tags";
import { EnvWithRouter } from "./router";
export class RouteComponent extends Component {
export class RouteComponent extends Component<{}, EnvWithRouter> {
static template = xml`
<t>
<t
+12 -4
View File
@@ -49,6 +49,10 @@ interface Options {
mode: Router["mode"];
}
export interface EnvWithRouter extends Env {
router: Router;
}
const paramRegexp = /\{\{(.*?)\}\}/;
export class Router {
@@ -60,7 +64,11 @@ export class Router {
routeIds: string[];
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;
this.mode = options.mode;
this.env = env as RouterEnv;
@@ -89,7 +97,7 @@ export class Router {
//--------------------------------------------------------------------------
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);
if (this.mode === "hash") {
window.addEventListener("hashchange", (this as any)._listener);
@@ -190,7 +198,7 @@ export class Router {
return {
type: "match",
route: route,
params: params
params: params,
};
}
}
@@ -215,7 +223,7 @@ export class Router {
const result = await route.beforeRouteEnter({
env: this.env,
from: this.currentRoute,
to: route
to: route,
});
if (result === false) {
return { type: "cancelled" };
+14 -10
View File
@@ -24,6 +24,10 @@ import { onWillUpdateProps } from "./hooks";
// Store Definition
//------------------------------------------------------------------------------
export interface EnvWithStore extends Env {
store: Store;
}
export type Action = ({ state, dispatch, env, getters }, ...payload: any) => any;
export type Getter = ({ state: any, getters }, payload?) => any;
@@ -49,7 +53,7 @@ export class Store extends Context {
if (config.getters) {
const firstArg = {
state: this.state,
getters: this.getters
getters: this.getters,
};
for (let g in config.getters) {
this.getters[g] = config.getters[g].bind(this, firstArg);
@@ -66,7 +70,7 @@ export class Store extends Context {
dispatch: this.dispatch.bind(this),
env: this.env,
state: this.state,
getters: this.getters
getters: this.getters,
},
...payload
);
@@ -83,7 +87,7 @@ interface SelectorOptions {
const isStrictEqual = (a, b) => a === b;
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 store = options.store || (component.env.store as Store);
if (!(store instanceof Store)) {
@@ -109,11 +113,11 @@ export function useStore(selector, options: SelectorOptions = {}): any {
}
return false;
}
store.updateFunctions[componentId].push(function(): boolean {
store.updateFunctions[componentId].push(function (): boolean {
return selectCompareUpdate(store!.state, component.props);
});
useContextWithCB(store, component, function(): Promise<void> | void {
useContextWithCB(store, component, function (): Promise<void> | void {
let shouldRender = false;
for (let fn of store.updateFunctions[componentId]) {
shouldRender = fn() || shouldRender;
@@ -122,12 +126,12 @@ export function useStore(selector, options: SelectorOptions = {}): any {
return component.render();
}
});
onWillUpdateProps(props => {
onWillUpdateProps((props) => {
selectCompareUpdate(store.state, props);
});
const __destroy = component.__destroy;
component.__destroy = parent => {
component.__destroy = (parent) => {
delete store.updateFunctions[componentId];
__destroy.call(component, parent);
};
@@ -144,16 +148,16 @@ export function useStore(selector, options: SelectorOptions = {}): any {
},
has(target, k) {
return k in result;
}
},
});
}
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);
}
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;
}
+11 -9
View File
@@ -10,14 +10,16 @@
* - debounce
*/
import { browser } from "./browser";
export function whenReady(fn?: any) {
return new Promise(function(resolve) {
return new Promise(function (resolve) {
if (document.readyState !== "loading") {
resolve();
} else {
document.addEventListener("DOMContentLoaded", resolve, false);
}
}).then(fn || function() {});
}).then(fn || function () {});
}
const loadedScripts: { [key: string]: Promise<void> } = {};
@@ -26,14 +28,14 @@ export function loadJS(url: string): Promise<void> {
if (url in loadedScripts) {
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");
script.type = "text/javascript";
script.src = url;
script.onload = function() {
script.onload = function () {
resolve();
};
script.onerror = function() {
script.onerror = function () {
reject(`Error loading file '${url}'`);
};
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> {
const result = await fetch(url);
const result = await browser.fetch(url);
if (!result.ok) {
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 {
let timeout;
return function(this: any) {
return function (this: any) {
const context = this;
const args = arguments;
function later() {
@@ -83,8 +85,8 @@ export function debounce(func: Function, wait: number, immediate?: boolean): Fun
}
}
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
browser.clearTimeout(timeout);
timeout = browser.setTimeout(later, wait);
if (callNow) {
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();
@@ -23,5 +23,9 @@ function htmlToVNode(node: ChildNode): VNode {
for (let c of node.childNodes) {
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 = {
create: updateProps,
update: updateProps
update: updateProps,
} as Module;
//------------------------------------------------------------------------------
@@ -151,7 +151,7 @@ function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
export const eventListenersModule = {
create: updateEventListeners,
update: updateEventListeners,
destroy: updateEventListeners
destroy: updateEventListeners,
} as Module;
//------------------------------------------------------------------------------
@@ -210,7 +210,7 @@ function updateAttrs(oldVnode: VNode, vnode: VNode): void {
export const attrsModule = {
create: updateAttrs,
update: updateAttrs
update: updateAttrs,
} as Module;
//------------------------------------------------------------------------------
+1 -1
View File
@@ -520,7 +520,7 @@ const htmlDomApi = {
parentNode,
nextSibling,
tagName,
setTextContent
setTextContent,
} as DOMAPI;
//------------------------------------------------------------------------------
+13 -13
View File
@@ -9,7 +9,7 @@ import {
patchNextFrame,
renderToDOM,
unpatchNextFrame,
nextTick
nextTick,
} from "./helpers";
//------------------------------------------------------------------------------
@@ -71,7 +71,7 @@ describe("animations", () => {
qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`);
let def = makeDeferred();
patchNextFrame(cb => {
patchNextFrame((cb) => {
expect(node.className).toBe("chimay-enter chimay-enter-active");
cb();
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>`);
let def = makeDeferred();
patchNextFrame(cb => {
patchNextFrame((cb) => {
expect(node.className).toBe("chimay-enter chimay-enter-active");
cb();
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>`);
let def = makeDeferred();
patchNextFrame(cb => {
patchNextFrame((cb) => {
expect(node.className).toBe("jupiler-enter jupiler-enter-active");
cb();
expect(node.className).toBe("");
@@ -139,7 +139,7 @@ describe("animations", () => {
// insert widget into the DOM
let def = makeDeferred();
var spanNode;
patchNextFrame(cb => {
patchNextFrame((cb) => {
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
cb();
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
@@ -155,7 +155,7 @@ describe("animations", () => {
// remove span from the DOM
def = makeDeferred();
widget.state.hide = true;
patchNextFrame(cb => {
patchNextFrame((cb) => {
expect(spanNode.className).toBe("chimay-leave chimay-leave-active");
cb();
expect(spanNode.className).toBe("chimay-leave-active chimay-leave-to");
@@ -182,7 +182,7 @@ describe("animations", () => {
// insert widget into the DOM
let def = makeDeferred();
var spanNode;
patchNextFrame(cb => {
patchNextFrame((cb) => {
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
cb();
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
@@ -210,7 +210,7 @@ describe("animations", () => {
let def = makeDeferred();
var spanNode;
patchNextFrame(cb => {
patchNextFrame((cb) => {
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
);
@@ -250,7 +250,7 @@ describe("animations", () => {
let def = makeDeferred();
var spanNode;
patchNextFrame(cb => {
patchNextFrame((cb) => {
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
);
@@ -275,7 +275,7 @@ describe("animations", () => {
// remove span from the DOM
def = makeDeferred();
widget.state.display = false;
patchNextFrame(cb => {
patchNextFrame((cb) => {
expect(fixture.innerHTML).toBe(
'<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 phase = "enter";
patchNextFrame(cb => {
patchNextFrame((cb) => {
let spans = fixture.querySelectorAll("span");
expect(spans.length).toBe(1);
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
@@ -366,7 +366,7 @@ describe("animations", () => {
let def = makeDeferred();
let phase = "enter";
patchNextFrame(cb => {
patchNextFrame((cb) => {
let spans = fixture.querySelectorAll("span");
expect(spans.length).toBe(1);
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
@@ -413,7 +413,7 @@ describe("animations", () => {
state = useState({ flag: false });
}
patchNextFrame(cb => cb());
patchNextFrame((cb) => cb());
const widget = new Parent();
await widget.mount(fixture);
@@ -1253,12 +1253,16 @@ exports[`other directives with t-component t-set can't alter from within callee
if (scope.iter != null) {
c2.push({text: scope.iter});
}
this.subTemplates['ChildWidget'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'}));
let c5 = [], p5 = {key:5};
let vn5 = h('p', p5, c5);
c1.push(vn5);
let _origScope4 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['2'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'}));
scope = _origScope4;
let c6 = [], p6 = {key:6};
let vn6 = h('p', p6, c6);
c1.push(vn6);
if (scope.iter != null) {
c5.push({text: scope.iter});
c6.push({text: scope.iter});
}
return vn1;
}"
@@ -1280,17 +1284,18 @@ exports[`other directives with t-component t-set can't alter in t-call body 1`]
if (scope.iter != null) {
c2.push({text: scope.iter});
}
let _origScope4 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
{
let _origScope4 = scope;
scope = Object.assign(Object.create(context), scope);
{
let c__0 = [];
utils.getScope(scope, 'iter').iter = 'inCall';
scope[utils.zero] = c__0;
}
this.subTemplates['ChildWidget'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'}));
scope = _origScope4;
this.constructor.subTemplates['2'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'}));
}
scope = _origScope4;
let c6 = [], p6 = {key:6};
let vn6 = h('p', p6, c6);
c1.push(vn6);
@@ -1400,6 +1405,47 @@ exports[`other directives with t-component t-set outside modified in t-foreach 1
}"
`;
exports[`props evaluation t-set with a body expression can be used as textual prop 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let c2 = new utils.VDomArray();
c2.push({text: \`42\`});
scope.abc = c2
// Component 'Child'
let w3 = '__4__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__4__']] : false;
let props3 = {val:scope.abc};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined);
let pvnode = w3.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| scope['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap['__4__'] = w3.__owl__.id;
let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__4__', hook: {remove() {},destroy(vn) {w3.destroy();}}});
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
w3.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`random stuff/miscellaneous can inject values in tagged templates 1`] = `
"function anonymous(context, extra
) {
@@ -1409,7 +1455,11 @@ exports[`random stuff/miscellaneous can inject values in tagged templates 1`] =
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
this.subTemplates['__template__1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'}));
let _origScope4 = scope;
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
this.constructor.subTemplates['3'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'}));
scope = _origScope4;
return vn1;
}"
`;
@@ -1682,6 +1732,33 @@ exports[`t-model directive basic use, on an input 1`] = `
}"
`;
exports[`t-model directive basic use, on an input with bracket expression 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('input', p2, c2);
c1.push(vn2);
let expr2 = scope['state'];
let exprKey2 = 'text';
p2.props = {value: expr2[exprKey2]};
extra.handlers['__3__'] = extra.handlers['__3__'] || ((ev) => {expr2[exprKey2] = ev.target.value});
p2.on['input'] = extra.handlers['__3__'];
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
let _5 = scope['state'].text;
if (_5 != null) {
c4.push({text: _5});
}
return vn1;
}"
`;
exports[`t-model directive basic use, on another key in component 1`] = `
"function anonymous(context, extra
) {
@@ -1748,6 +1825,46 @@ exports[`t-model directive in a t-foreach 1`] = `
}"
`;
exports[`t-model directive in a t-foreach, part 2 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let _2 = scope['state'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
}
let _length3 = _3.length;
let _origScope5 = scope;
scope = Object.create(scope);
for (let i1 = 0; i1 < _length3; i1++) {
scope.thing_first = i1 === 0
scope.thing_last = i1 === _length3 - 1
scope.thing_index = i1
scope.thing = _3[i1]
scope.thing_value = _4[i1]
let key1 = scope['thing_index'];
let c6 = [], p6 = {key:\`\${key1}_6\`,on:{}};
let vn6 = h('input', p6, c6);
c1.push(vn6);
let expr6 = scope['state'];
let exprKey6 = scope['thing_index'];
let k7 = \`__7__\${key1}__\`;
p6.props = {value: expr6[exprKey6]};
extra.handlers[k7] = extra.handlers[k7] || ((ev) => {expr6[exprKey6] = ev.target.value});
p6.on['input'] = extra.handlers[k7];
}
scope = _origScope5;
return vn1;
}"
`;
exports[`t-model directive on a select 1`] = `
"function anonymous(context, extra
) {
@@ -1832,7 +1949,7 @@ exports[`t-model directive on an input type=radio 1`] = `
let _2 = 'radio';
let _3 = 'one';
let _4 = 'One';
let c5 = [], p5 = {key:5,attrs:{type: _2,id: _3,value: _4},on:{}};
let c5 = [], p5 = {key:5,attrs:{type: _2,id: _3,value: _4},props:{value: _4},on:{}};
let vn5 = h('input', p5, c5);
c1.push(vn5);
let expr5 = scope['state'];
@@ -1842,7 +1959,7 @@ exports[`t-model directive on an input type=radio 1`] = `
let _7 = 'radio';
let _8 = 'two';
let _9 = 'Two';
let c10 = [], p10 = {key:10,attrs:{type: _7,id: _8,value: _9},on:{}};
let c10 = [], p10 = {key:10,attrs:{type: _7,id: _8,value: _9},props:{value: _9},on:{}};
let vn10 = h('input', p10, c10);
c1.push(vn10);
let expr10 = scope['state'];
@@ -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`] = `
"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`] = `
"function anonymous(context, extra
) {
+24 -2
View File
@@ -27,7 +27,7 @@ afterEach(() => {
function children(w: Component): Component[] {
const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]);
return Object.keys(childrenMap).map((id) => childrenMap[id]);
}
describe("async rendering", () => {
@@ -404,7 +404,7 @@ describe("async rendering", () => {
}
class ChildB extends Component {
willStart(): any {
return new Promise(function() {});
return new Promise(function () {});
}
}
class Parent extends Component {
@@ -1366,6 +1366,28 @@ describe("async rendering", () => {
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 () => {
class Widget extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`;
+237 -34
View File
@@ -1,16 +1,16 @@
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 { xml } from "../../src/tags";
import { useState, useRef } from "../../src/hooks";
import { EventBus } from "../../src/core/event_bus";
import {
editInput,
makeDeferred,
makeTestFixture,
makeTestEnv,
makeTestFixture,
nextMicroTick,
nextTick,
normalize,
editInput
} from "../helpers";
//------------------------------------------------------------------------------
@@ -39,7 +39,7 @@ afterEach(() => {
function children(w: Component): Component[] {
const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]);
return Object.keys(childrenMap).map((id) => childrenMap[id]);
}
// Test components
@@ -133,7 +133,7 @@ describe("basic widget properties", () => {
static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
state = useState({
counter: 0
counter: 0,
});
}
@@ -167,7 +167,7 @@ describe("basic widget properties", () => {
static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
state = useState({
counter: 0
counter: 0,
});
}
@@ -537,7 +537,7 @@ describe("lifecycle hooks", () => {
"child:mounted",
"parent:willUnmount",
"child:willUnmount",
"childchild:willUnmount"
"childchild:willUnmount",
]);
});
@@ -589,7 +589,7 @@ describe("lifecycle hooks", () => {
"childchild:willPatch",
"childchild:patched",
"child:patched",
"parent:patched"
"parent:patched",
]);
});
@@ -782,7 +782,7 @@ describe("lifecycle hooks", () => {
"c mounted",
"p mounted",
"p willunmount",
"c willunmount"
"c willunmount",
]);
});
@@ -960,7 +960,7 @@ describe("lifecycle hooks", () => {
"parent:willPatch",
"child:willPatch",
"child:patched",
"parent:patched"
"parent:patched",
]);
});
});
@@ -1134,7 +1134,7 @@ describe("composition", () => {
class App extends Component {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = useState({
child: "a"
child: "a",
});
get myComponent() {
return this.state.child === "a" ? A : B;
@@ -1158,7 +1158,7 @@ describe("composition", () => {
class App extends Component {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = useState({
child: "a"
child: "a",
});
get myComponent() {
return this.state.child === "a" ? A : B;
@@ -1228,7 +1228,7 @@ describe("composition", () => {
static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
state = useState({
counter: 0
counter: 0,
});
}
@@ -1327,7 +1327,7 @@ describe("composition", () => {
static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
state = useState({
counter: 0
counter: 0,
});
}
@@ -1350,7 +1350,7 @@ describe("composition", () => {
static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
state = useState({
counter: 0
counter: 0,
});
}
@@ -1389,7 +1389,7 @@ describe("composition", () => {
);
class Parent extends Component {
state = useState({
numbers: [1, 2, 3]
numbers: [1, 2, 3],
});
static components = { ChildWidget };
}
@@ -1429,7 +1429,7 @@ describe("composition", () => {
class Parent extends Component {
static template = "parent";
state = useState({
numbers: [1, 2, 3]
numbers: [1, 2, 3],
});
static components = { ChildWidget };
}
@@ -1513,8 +1513,8 @@ describe("composition", () => {
blips: [
{ a: "a", id: 1 },
{ b: "b", id: 2 },
{ c: "c", id: 4 }
]
{ c: "c", id: 4 },
],
});
}
const parent = new Parent();
@@ -1621,8 +1621,8 @@ describe("composition", () => {
records: [
{ id: 1, val: 1 },
{ id: 2, val: 2 },
{ id: 3, val: 3 }
]
{ id: 3, val: 3 },
],
});
static components = { ChildWidget };
}
@@ -1725,6 +1725,47 @@ describe("props evaluation ", () => {
await widget.mount(fixture);
expect(normalize(fixture.innerHTML)).toBe("<div><span>42</span></div>");
});
test("t-set with a body expression can be used as textual prop", async () => {
class Child extends Component {
static template = xml`<span t-esc="props.val"/>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
<t t-set="abc">42</t>
<Child val="abc"/>
</div>`;
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
});
test("t-set with a body expression can be passed in props, and then t-raw", async () => {
class Child extends Component {
static template = xml`
<span>
<t t-esc="props.val"/>
<t t-raw="props.val"/>
</span>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
<t t-set="abc"><p>43</p></t>
<Child val="abc"/>
</div>`;
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>&lt;p&gt;4343&lt;/p&gt;<p>43</p></span></div>");
});
});
describe("other directives with t-component", () => {
@@ -2003,7 +2044,7 @@ describe("other directives with t-component", () => {
}
const widget = new ParentWidget();
await widget.mount(fixture);
(<HTMLElement>fixture).addEventListener("ev", function(e) {
(<HTMLElement>fixture).addEventListener("ev", function (e) {
steps.push(e.defaultPrevented);
});
@@ -2038,7 +2079,7 @@ describe("other directives with t-component", () => {
}
const widget = new ParentWidget();
await widget.mount(fixture);
(<HTMLElement>fixture).addEventListener("ev", function(e) {
(<HTMLElement>fixture).addEventListener("ev", function (e) {
steps.push(e.defaultPrevented);
});
@@ -2976,7 +3017,7 @@ describe("random stuff/miscellaneous", () => {
"D:mounted",
"C:mounted",
"B:mounted",
"A:mounted"
"A:mounted",
]);
// update
@@ -2999,7 +3040,7 @@ describe("random stuff/miscellaneous", () => {
"E:destroy",
"F:mounted",
"D:patched",
"C:patched"
"C:patched",
]);
});
@@ -3015,6 +3056,85 @@ describe("random stuff/miscellaneous", () => {
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
});
test("update props of component without concrete own node", async () => {
class Custom extends Component {
static template = xml`
<div class="widget-subkey">
<t t-esc="props.key"/>__<t t-esc="props.subKey"/>
</div>`;
}
class Child extends Component {
static components = { Custom };
static template = xml`
<t t-component="Custom"
t-key="props.subKey"
key="props.key"
subKey="props.subKey"/>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
<Child t-key="childProps.key" t-props="childProps"/>
</div>`;
childProps = {
key: 1,
subKey: 1,
};
}
const parent = new Parent(null);
await parent.mount(fixture);
expect(fixture.textContent!.trim()).toBe("1__1");
// First step: change the Custom's instance
Object.assign(parent.childProps, {
subKey: 2,
});
parent.render();
await nextTick();
expect(fixture.textContent!.trim()).toBe("1__2");
// Second step, change both Child's and Custom's instance
Object.assign(parent.childProps, {
key: 2,
subKey: 3,
});
parent.render();
await nextTick();
expect(fixture.textContent!.trim()).toBe("2__3");
});
test("two renderings initiated between willPatch and patched", async () => {
let app;
class Panel extends Component {
static template = xml`<abc><t t-esc="props.val"/></abc>`;
mounted() {
app.render();
}
willUnmount() {
app.render();
}
}
// Main root component
class App extends Component {
static components = { Panel };
static template = xml`<div><Panel t-key="'panel_' + state.panel" val="state.panel"/></div>`;
state = useState({ panel: "Panel1" });
}
app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><abc>Panel1</abc></div>");
app.state.panel = "Panel2";
await nextTick();
expect(fixture.innerHTML).toBe("<div><abc>Panel2</abc></div>");
});
});
describe("widget and observable state", () => {
@@ -3080,6 +3200,15 @@ describe("can deduce template from name", () => {
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 () => {
class ABC extends Component {
static template = "Achel";
@@ -3129,6 +3258,46 @@ describe("t-model directive", () => {
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
});
test("basic use, on an input with bracket expression", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state['text']"/>
<span><t t-esc="state.text"/></span>
</div>`;
state = useState({ text: "" });
}
const comp = new SomeComponent();
await comp.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
const input = fixture.querySelector("input")!;
await editInput(input, "test");
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
});
test("throws if invalid expression", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state"/>
</div>`;
state = useState({ text: "" });
}
const comp = new SomeComponent();
let error;
try {
await comp.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Invalid t-model expression: "state" (it should be assignable)`);
});
test("basic use, on another key in component", async () => {
env.qweb.addTemplates(`
<templates>
@@ -3401,7 +3570,7 @@ describe("t-model directive", () => {
state = useState([
{ f: false, id: 1 },
{ f: false, id: 2 },
{ f: false, id: 3 }
{ f: false, id: 3 },
]);
}
const comp = new SomeComponent();
@@ -3418,6 +3587,28 @@ describe("t-model directive", () => {
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
});
test("in a t-foreach, part 2", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<t t-foreach="state" t-as="thing" t-key="thing_index" >
<input t-model="state[thing_index]"/>
</t>
</div>
`;
state = useState(["zuko", "iroh"]);
}
const comp = new SomeComponent();
await comp.mount(fixture);
expect(comp.state).toEqual(["zuko", "iroh"]);
const input = fixture.querySelectorAll("input")[1]!;
input.value = "uncle iroh";
input.dispatchEvent(new Event("input"));
expect(comp.state).toEqual(["zuko", "uncle iroh"]);
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
});
test("two inputs in a div with a t-key", async () => {
class SomeComponent extends Component {
static template = xml`
@@ -3446,7 +3637,7 @@ describe("environment and plugins", () => {
let bus = new EventBus();
// definition of a plugin
const somePlugin = env => {
const somePlugin = (env) => {
env.someFlag = true;
bus.on("some-event", null, () => {
env.someFlag = !env.someFlag;
@@ -3475,11 +3666,14 @@ describe("environment and plugins", () => {
});
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>`;
}
App1.env = { test: 1 };
class App2 extends Component {
class App2 extends Component<{}, AppEnv> {
static template = xml`<span></span>`;
}
App2.env = { test: 2 };
@@ -3813,7 +4007,9 @@ describe("t-call", () => {
expect(fixture.innerHTML).toBe("<div><p>lucas</p></div>");
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 () => {
@@ -3835,7 +4031,10 @@ describe("t-call", () => {
expect(fixture.innerHTML).toBe("<div><span>lucas</span></div>");
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 () => {
@@ -3877,7 +4076,9 @@ describe("t-call", () => {
expect(fixture.innerHTML).toBe("<span>lucas</span>");
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 () => {
@@ -3893,7 +4094,9 @@ describe("t-call", () => {
}
const parent = new Parent();
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();
});
+40 -15
View File
@@ -92,7 +92,7 @@ describe("props validation", () => {
{ type: String, ok: "1", ko: 1 },
{ type: Object, ok: {}, ko: "1" },
{ type: Date, ok: new Date(), ko: "1" },
{ type: Function, ok: () => {}, ko: "1" }
{ type: Function, ok: () => {}, ko: "1" },
];
let props;
@@ -149,7 +149,7 @@ describe("props validation", () => {
{ type: String, ok: "1", ko: 1 },
{ type: Object, ok: {}, ko: "1" },
{ type: Date, ok: new Date(), ko: "1" },
{ type: Function, ok: () => {}, ko: "1" }
{ type: Function, ok: () => {}, ko: "1" },
];
let props;
@@ -396,7 +396,7 @@ describe("props validation", () => {
class TestWidget extends Component {
static template = xml`<div>hey</div>`;
static props = {
p: { type: Object, shape: { id: Number, url: String } }
p: { type: Object, shape: { id: Number, url: String } },
};
}
class Parent extends Component {
@@ -458,9 +458,9 @@ describe("props validation", () => {
type: Object,
shape: {
id: Number,
url: [Boolean, { type: Array, element: Number }]
}
}
url: [Boolean, { type: Array, element: Number }],
},
},
};
}
class Parent extends Component {
@@ -510,10 +510,10 @@ describe("props validation", () => {
element: {
type: Object,
shape: {
num: { type: Number, optional: true }
}
}
}
num: { type: Number, optional: true },
},
},
},
};
}
let error;
@@ -539,8 +539,8 @@ describe("props validation", () => {
class TestComponent extends Component {
static props = {
size: {
validate: e => ["small", "medium", "large"].includes(e)
}
validate: (e) => ["small", "medium", "large"].includes(e),
},
};
}
let error;
@@ -561,13 +561,13 @@ describe("props validation", () => {
});
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 {
static props = {
n: {
type: Number,
validate: validator
}
validate: validator,
},
};
}
let error;
@@ -786,6 +786,31 @@ describe("props validation", () => {
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
});
test("mix of optional and mandatory", async () => {
class Child extends Component {
static props = {
optional: { type: String, optional: true },
mandatory: Number,
};
static template = xml` <div><t t-esc="props.mandatory"/></div>`;
}
class App extends Component {
static components = { Child };
static template = xml`<div><Child/></div>`;
}
const w = new App(undefined, {});
let error;
try {
await w.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Missing props 'mandatory' (component 'Child')");
});
});
describe("default props", () => {
+129 -15
View File
@@ -28,7 +28,7 @@ afterEach(() => {
function children(w: Component): Component[] {
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>
<div t-name="Parent">
<Dialog>
<t t-set="header"><span>header</span></t>
<t t-set="footer"><span>footer</span></t>
<t t-set-slot="header"><span>header</span></t>
<t t-set-slot="footer"><span>footer</span></t>
</Dialog>
</div>
<div t-name="Dialog">
@@ -67,6 +67,38 @@ describe("t-slot directive", () => {
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 () => {
class Dialog extends Component {
static template = xml`
@@ -128,7 +160,7 @@ describe("t-slot directive", () => {
</span>`;
}
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 };
}
const parent = new Parent();
@@ -143,7 +175,7 @@ describe("t-slot directive", () => {
<div t-name="Parent">
<span class="counter"><t t-esc="state.val"/></span>
<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>
</div>
<span t-name="Dialog"><t t-slot="footer"/></span>
@@ -192,8 +224,8 @@ describe("t-slot directive", () => {
state = useState({
users: [
{ id: 1, name: "Aaron" },
{ id: 2, name: "David" }
]
{ id: 2, name: "David" },
],
});
static components = { Link };
}
@@ -236,8 +268,8 @@ describe("t-slot directive", () => {
state = useState({
users: [
{ id: 1, name: "Aaron" },
{ id: 2, name: "David" }
]
{ id: 2, name: "David" },
],
});
static components = { Link };
}
@@ -302,7 +334,7 @@ describe("t-slot directive", () => {
<div>
<span class="counter"><t t-esc="state.val"/></span>
<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>
</div>
`;
@@ -376,7 +408,7 @@ describe("t-slot directive", () => {
<templates>
<div t-name="Parent">
<Dialog>
<t t-set="content">
<t t-set-slot="content">
<span>sts</span>
<span>rocks</span>
</t>
@@ -442,14 +474,14 @@ describe("t-slot directive", () => {
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;
console.log = jest.fn();
env.qweb.addTemplates(`
<templates>
<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>
<span t-name="Dialog">
<t t-slot="content"/>
@@ -623,8 +655,8 @@ describe("t-slot directive", () => {
class A extends Component {
static template = xml`
<B>
<t t-set="s1"><C val="1"/></t>
<t t-set="s2"><C val="2"/></t>
<t t-set-slot="s1"><C val="1"/></t>
<t t-set-slot="s2"><C val="2"/></t>
</B>`;
static components = { B, C };
}
@@ -873,4 +905,86 @@ describe("t-slot directive", () => {
await parent.mount(fixture);
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();
});
test("slot and t-esc", async () => {
class Dialog extends Component {
static template = xml`<span><t t-slot="default"/></span>`;
}
class Parent extends Component {
static template = xml`<div><Dialog><t t-esc="'toph'"/></Dialog></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>toph</span></div>");
});
test("slot and (inline) t-esc", async () => {
class Dialog extends Component {
static template = xml`<span><t t-slot="default"/></span>`;
}
class Parent extends Component {
static template = xml`<div><Dialog t-esc="'toph'"/></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>toph</span></div>");
});
test("slot and t-call", async () => {
env.qweb.addTemplate("sokka", "<p>sokka</p>");
class Dialog extends Component {
static template = xml`<span><t t-slot="default"/></span>`;
}
class Parent extends Component {
static template = xml`<div><Dialog><t t-call="sokka"/></Dialog></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span><p>sokka</p></span></div>");
});
test("slot and (inline) t-call", async () => {
env.qweb.addTemplate("sokka", "<p>sokka</p>");
class Dialog extends Component {
static template = xml`<span><t t-slot="default"/></span>`;
}
class Parent extends Component {
static template = xml`<div><Dialog t-call="sokka"/></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span><p>sokka</p></span></div>");
});
});
+141 -3
View File
@@ -2,6 +2,7 @@ import { Component, Env } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeDeferred, makeTestEnv, makeTestFixture, nextTick, nextMicroTick } from "../helpers";
import { scheduler } from "../../src/component/scheduler";
//------------------------------------------------------------------------------
// Setup and helpers
@@ -28,14 +29,34 @@ afterEach(() => {
describe("mount targets", () => {
test("can attach a component to an existing node (if same tagname)", async () => {
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");
div.classList.add("arbitrary");
div.innerHTML = `<p>pre-existing</p>`;
fixture.appendChild(div);
const app = new App();
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 () => {
@@ -315,7 +336,7 @@ describe("unmounting and remounting", () => {
class Child extends Component {
static template = xml`<span t-esc="state.val"/>`;
state = useState({
val: "C1"
val: "C1",
});
constructor(parent, props) {
super(parent, props);
@@ -470,4 +491,121 @@ describe("unmounting and remounting", () => {
expect(steps).toEqual(["1 resolved", "2 resolved"]);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
});
test("mounting a destroyed widget", async () => {
class MyWidget extends Component {
static template = xml`<div>Hey</div>`;
}
const w = new MyWidget();
w.destroy(); // because, why not
let error;
try {
await w.mount(fixture);
} catch (e) {
error = e;
}
expect(scheduler.tasks.length).toBe(0);
expect(error).toBeDefined();
expect(error.message).toBe("Cannot mount a destroyed component");
});
test("destroying a sub-component cleans itself from parent's vnode", async () => {
class C1 extends Component {
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
}
class P extends Component {
static components = { C1 };
static template = xml`<div><div><C1 t-props="state" t-if="state.a"/></div></div>`;
state = {
a: "first",
};
}
const parent = new P();
await parent.mount(fixture);
expect(fixture.textContent).toBe("first");
parent.unmount();
parent.state.a = "";
parent.mount(fixture);
parent.state.a = "fixed";
await parent.render();
expect(fixture.textContent).toBe("fixed");
});
test("destroying a sub-component cleans itself from parent's vnode, part 2", async () => {
class C1 extends Component {
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
}
class P extends Component {
static components = { C1 };
static template = xml`<div><div><C1 t-props="state" t-if="state.a"/>some text</div></div>`;
state = {
a: "first",
};
}
const parent = new P();
await parent.mount(fixture);
expect(fixture.textContent).toBe("firstsome text");
parent.unmount();
parent.state.a = "";
parent.mount(fixture);
parent.state.a = "fixed";
await parent.render();
expect(fixture.textContent).toBe("fixedsome text");
});
test("destroying a sub-component cleans itself from parent's vnode, part 3", async () => {
class C1 extends Component {
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
}
class C2 extends Component {
static template = xml`<C1 a="props.a"/>`;
static components = { C1 };
}
class P extends Component {
static components = { C2 };
static template = xml`<div><div><C2 t-props="state" t-if="state.a"/></div></div>`;
state = {
a: "first",
};
}
const parent = new P();
await parent.mount(fixture);
expect(fixture.textContent).toBe("first");
parent.unmount();
parent.state.a = "";
parent.mount(fixture);
parent.state.a = "fixed";
await parent.render();
expect(fixture.textContent).toBe("fixed");
});
test("destroying a sub-component cleans itself from parent's vnode, part 4", async () => {
class C1 extends Component {
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
}
class C2 extends Component {
static template = xml`<C1 a="props.a"/>`;
static components = { C1 };
}
class P extends Component {
static components = { C2 };
static template = xml`<div><div><C2 t-props="state" t-if="state.a"/>some text</div></div>`;
state = {
a: "first",
};
}
const parent = new P();
await parent.mount(fixture);
expect(fixture.textContent).toBe("firstsome text");
parent.unmount();
parent.state.a = "";
parent.mount(fixture);
parent.state.a = "fixed";
await parent.render();
expect(fixture.textContent).toBe("fixedsome text");
});
});
+1 -1
View File
@@ -14,7 +14,7 @@ describe("event bus behaviour", () => {
expect.assertions(1);
const bus = new EventBus();
const owner = {};
bus.on("event", owner, function(this: any) {
bus.on("event", owner, function (this: any) {
expect(this).toBe(owner);
});
bus.trigger("event");
+19 -2
View File
@@ -22,7 +22,7 @@ describe("observer", () => {
expect(observer.rev).toBe(2);
expect(obj2).toEqual({
a: 2
a: 2,
});
});
@@ -47,7 +47,7 @@ describe("observer", () => {
expect(observer.rev).toBe(5);
expect(obj).toEqual({
a: null,
b: undefined
b: undefined,
});
});
@@ -68,6 +68,23 @@ describe("observer", () => {
expect(obj.date).not.toBe(date);
});
test("properly handle promises (i.e.: treat them like primitive values", async () => {
const observer = new Observer();
let resolved = false;
const prom = new Promise((r) => r());
const obj: any = observer.observe({ prom });
expect(obj.prom).toBeInstanceOf(Promise);
obj.prom.then(() => (resolved = true));
expect(observer.revNumber(obj)).toBe(1);
expect(resolved).toBe(false);
await Promise.resolve();
expect(resolved).toBe(true);
expect(observer.revNumber(obj)).toBe(1);
});
test("can change values in array", () => {
const observer = new Observer();
const obj: any = observer.observe({ arr: [1, 2] });
+5 -10
View File
@@ -6,12 +6,7 @@ import { patch } from "../src/vdom";
import "../src/qweb/base_directives";
import "../src/qweb/extensions";
import "../src/component/directive";
// modifies scheduler to make it faster to test components
scheduler.requestAnimationFrame = function(callback: FrameRequestCallback) {
setTimeout(callback, 1);
return 1;
};
import { browser } from "../src/browser";
// Some static cleanup
let nextSlotId;
@@ -42,9 +37,8 @@ export function nextMicroTick(): Promise<void> {
}
export async function nextTick(): Promise<void> {
return new Promise(function(resolve) {
setTimeout(() => scheduler.requestAnimationFrame(() => resolve()));
});
await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
await new Promise((resolve) => setTimeout(resolve));
}
export function makeTestFixture() {
@@ -75,7 +69,8 @@ export function makeDeferred(): Deferred {
export function makeTestEnv(): Env {
return {
qweb: new QWeb()
qweb: new QWeb(),
browser: browser,
};
}
+40 -7
View File
@@ -10,7 +10,7 @@ import {
onWillStart,
onWillUpdateProps,
useSubEnv,
useExternalListener
useExternalListener,
} from "../src/hooks";
import { xml } from "../src/tags";
@@ -214,7 +214,7 @@ describe("hooks", () => {
"hook:mounted1",
"hook:mounted2",
"hook:willunmount2",
"hook:willunmount1"
"hook:willunmount1",
]);
});
@@ -267,7 +267,7 @@ describe("hooks", () => {
class WidgetC extends Component {
static template = xml`<div class="outer-div">Hello<WidgetB t-ref="mywidgetb" /></div>`;
static components = { WidgetB };
ref = useRef("mywidgetb");
ref = useRef<WidgetB>("mywidgetb");
}
const widget = new WidgetC();
@@ -559,20 +559,39 @@ describe("hooks", () => {
test("can use onWillStart, onWillUpdateProps", async () => {
const steps: string[] = [];
async function slow(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve("slow");
}, 0);
});
}
function useMyHook() {
onWillStart(() => {
onWillStart(async () => {
steps.push(await slow());
steps.push("onWillStart");
});
onWillUpdateProps(nextProps => {
onWillUpdateProps(async (nextProps) => {
expect(nextProps).toEqual({ value: 2 });
steps.push(await slow());
steps.push("onWillUpdateProps");
});
}
function use2ndHook() {
onWillStart(() => {
steps.push("on2ndStart");
});
onWillUpdateProps((nextProps) => {
expect(nextProps).toEqual({ value: 2 });
steps.push("on2ndUpdate");
});
}
class MyComponent extends Component {
static template = xml`<span><t t-esc="props.value"/></span>`;
constructor(parent, props) {
super(parent, props);
useMyHook();
use2ndHook();
}
}
class App extends Component {
@@ -586,12 +605,26 @@ describe("hooks", () => {
expect(app).not.toHaveProperty("willStart");
expect(app).not.toHaveProperty("willUpdateProps");
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;
await nextTick();
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 () => {
+4 -4
View File
@@ -486,7 +486,7 @@ describe("Portal: Basic use and DOM placement", () => {
"parent:mounted",
"parent:willPatch",
"child:mounted",
"parent:patched"
"parent:patched",
]);
parent.state.val = 2;
@@ -499,7 +499,7 @@ describe("Portal: Basic use and DOM placement", () => {
"parent:willPatch",
"child:willPatch",
"child:patched",
"parent:patched"
"parent:patched",
]);
parent.state.hasChild = false;
@@ -515,7 +515,7 @@ describe("Portal: Basic use and DOM placement", () => {
"parent:patched",
"parent:willPatch",
"child:willUnmount",
"parent:patched"
"parent:patched",
]);
});
@@ -741,7 +741,7 @@ describe("Portal: Events handling", () => {
steps.push(ev.type as string);
}
}
const bodyListener = ev => {
const bodyListener = (ev) => {
steps.push(`body: ${ev.type}`);
};
document.body.addEventListener("click", bodyListener);
File diff suppressed because it is too large Load Diff
+213 -53
View File
@@ -195,7 +195,7 @@ describe("t-esc", () => {
v2: undefined,
v3: null,
v4: 0,
v5: ""
v5: "",
};
expect(renderToString(qweb, "test", vals)).toBe(
"<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] } });
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", () => {
@@ -486,7 +493,7 @@ describe("t-if", () => {
cond3: false,
cond4: false,
m: 5,
n: 2
n: 2,
};
expect(normalize(renderToString(qweb, "test", context))).toBe("<div>andormgtnlt</div>");
});
@@ -625,7 +632,7 @@ describe("attributes", () => {
test("object", () => {
qweb.addTemplate("test", `<div t-att="value"/>`);
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>`);
});
@@ -684,7 +691,7 @@ describe("attributes", () => {
const result = renderToString(qweb, "test", {
value1: 0,
value2: 1,
value3: 2
value3: 2,
});
expect(result).toBe(`<div foo="a 0 is 1 of 2 ]"></div>`);
});
@@ -703,7 +710,7 @@ describe("attributes", () => {
const result = renderToString(qweb, "test", {
bar: 0,
baz: 1,
qux: { qux: "<>" }
qux: { qux: "<>" },
});
const expected = '<div foo="<foo" bar="0" baz="<1>" qux="<>"></div>';
expect(result).toBe(expected);
@@ -715,6 +722,12 @@ describe("attributes", () => {
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", () => {
qweb.addTemplate("test", `<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`);
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>');
const expected = "<div><span>ok</span></div>";
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", () => {
@@ -742,7 +755,7 @@ describe("t-call (template calling", () => {
qweb.addTemplate("caller", '<t t-call="_basic-callee"/>');
const expected = "<div>ok</div>";
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", () => {
@@ -750,13 +763,14 @@ describe("t-call (template calling", () => {
qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>');
const expected = "<div><span>ok</span></div>";
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", () => {
qweb.addTemplate("_basic-callee", "<t>ok</t>");
test("t-call allowed on a non t node", () => {
qweb.addTemplate("_basic-callee", "<span>ok</span>");
qweb.addTemplate("caller", '<div t-call="_basic-callee"/>');
expect(() => renderToString(qweb, "caller")).toThrow("Invalid tag");
const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller")).toBe(expected);
});
test("with unused body", () => {
@@ -885,7 +899,8 @@ describe("t-call (template calling", () => {
`);
const expected = "<div><span>hey</span></div>";
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();
});
@@ -913,7 +928,8 @@ describe("t-call (template calling", () => {
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).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();
});
@@ -940,7 +956,41 @@ describe("t-call (template calling", () => {
const expected =
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>";
expect(renderToString(qweb, "Parent", { root }, { 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();
});
@@ -1012,6 +1062,32 @@ describe("t-call (template calling", () => {
const context = { list: [{ val: 1 }, { val: 2 }, { val: 3 }] };
expect(trim(renderToString(qweb, "wrapper", context))).toBe(expected);
});
test("two different QWeb instances, and shared templates", () => {
QWeb.registerTemplate("sub", `<span>ok</span>`);
QWeb.registerTemplate("main", `<div><t t-call="sub"/></div>`);
const qweb1 = new QWeb();
const qweb2 = new QWeb();
expect(renderToString(qweb1, "main")).toBe("<div><span>ok</span></div>");
expect(renderToString(qweb2, "main")).toBe("<div><span>ok</span></div>");
});
test("t-call with t-set inside and body text content", () => {
qweb.addTemplate("sub", `<p><t t-esc="val"/></p>`);
qweb.addTemplate(
"main",
`
<div>
<t t-call="sub">
<t t-set="val">yip yip</t>
</t>
</div>`
);
const expected = "<div><p>yip yip</p></div>";
expect(renderToString(qweb, "main")).toBe(expected);
});
});
describe("foreach", () => {
@@ -1102,6 +1178,63 @@ describe("foreach", () => {
);
});
test("t-call without body in t-foreach in t-foreach", () => {
qweb.addTemplate(
"test_called",
`<t>
<t t-set="c" t-value="'x' + '_' + a + '_'+ b" />
[<t t-esc="a" />]
[<t t-esc="b" />]
[<t t-esc="c" />]
</t>`
);
qweb.addTemplate(
"test",
`<div>
<t t-foreach="numbers" t-as="a">
<t t-foreach="letters" t-as="b">
<t t-call="test_called" />
</t>
<span t-esc="c"/>
</t>
<span>[<t t-esc="a" />][<t t-esc="b" />][<t t-esc="c" />]</span>
</div>`
);
const context = { numbers: [1, 2, 3], letters: ["a", "b"] };
expect(renderToString(qweb, "test", context)).toBe(
"<div> [1] [a] [x_1_a] [1] [b] [x_1_b] <span></span> [2] [a] [x_2_a] [2] [b] [x_2_b] <span></span> [3] [a] [x_3_a] [3] [b] [x_3_b] <span></span><span>[][][]</span></div>"
);
});
test("t-call with body in t-foreach in t-foreach", () => {
qweb.addTemplate(
"test_called",
`<t>
[<t t-esc="a" />]
[<t t-esc="b" />]
[<t t-esc="c" />]
</t>`
);
qweb.addTemplate(
"test",
`<div>
<t t-foreach="numbers" t-as="a">
<t t-foreach="letters" t-as="b">
<t t-call="test_called" >
<t t-set="c" t-value="'x' + '_' + a + '_'+ b" />
</t>
</t>
<span t-esc="c"/>
</t>
<span>[<t t-esc="a" />][<t t-esc="b" />][<t t-esc="c" />]</span>
</div>`
);
const context = { numbers: [1, 2, 3], letters: ["a", "b"] };
expect(renderToString(qweb, "test", context)).toBe(
"<div> [1] [a] [x_1_a] [1] [b] [x_1_b] <span></span> [2] [a] [x_2_a] [2] [b] [x_2_b] <span></span> [3] [a] [x_3_a] [3] [b] [x_3_b] <span></span><span>[][][]</span></div>"
);
});
test("throws error if invalid loop expression", () => {
qweb.addTemplate(
"test",
@@ -1197,7 +1330,7 @@ describe("t-on", () => {
{
add() {
a = 3;
}
},
},
{ handlers: [] }
);
@@ -1220,7 +1353,7 @@ describe("t-on", () => {
},
handleDblClick() {
steps.push("dblclick");
}
},
},
{ handlers: [] }
);
@@ -1240,7 +1373,7 @@ describe("t-on", () => {
{
add(n) {
a = a + n;
}
},
},
{ handlers: [] }
);
@@ -1257,7 +1390,7 @@ describe("t-on", () => {
{
add({ val }) {
a = a + val;
}
},
},
{ handlers: [] }
);
@@ -1274,7 +1407,7 @@ describe("t-on", () => {
{
doSomething(arg) {
expect(arg).toEqual({});
}
},
},
{ handlers: [] }
);
@@ -1290,7 +1423,7 @@ describe("t-on", () => {
{
doSomething(arg) {
expect(arg).toEqual({});
}
},
},
{ handlers: [] }
);
@@ -1312,7 +1445,7 @@ describe("t-on", () => {
{
activate(action) {
expect(action).toBe("someval");
}
},
},
{ handlers: [] }
);
@@ -1325,7 +1458,7 @@ describe("t-on", () => {
let owner = {
add() {
expect(this).toBe(owner);
}
},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
(<HTMLElement>node).click();
@@ -1335,8 +1468,8 @@ describe("t-on", () => {
qweb.addTemplate("test", `<button t-on-click="state.counter++">Click</button>`);
let owner = {
state: {
counter: 0
}
counter: 0,
},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.counter).toBe(0);
@@ -1349,10 +1482,10 @@ describe("t-on", () => {
let owner = {
state: {
counter: 0,
incrementCounter: inc => {
incrementCounter: (inc) => {
owner.state.counter += inc;
}
}
},
},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.counter).toBe(0);
@@ -1364,8 +1497,8 @@ describe("t-on", () => {
qweb.addTemplate("test", `<button t-on-click="state.flag = !state.flag">Toggle</button>`);
let owner = {
state: {
flag: true
}
flag: true,
},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.flag).toBe(true);
@@ -1382,8 +1515,8 @@ describe("t-on", () => {
return n + 1;
},
state: {
n: 11
}
n: 11,
},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.n).toBe(11);
@@ -1399,7 +1532,7 @@ describe("t-on", () => {
let owner = {
update() {
expect(this).toBe(owner);
}
},
};
const node = renderToDOM(qweb, "main", owner, { handlers: [] });
@@ -1416,7 +1549,7 @@ describe("t-on", () => {
expect(this).toBe(owner);
expect(val).toBe(444);
},
value: 444
value: 444,
};
const node = renderToDOM(qweb, "main", owner, { handlers: [] });
@@ -1445,7 +1578,7 @@ describe("t-on", () => {
onClickPreventedAndStopped(e) {
expect(e.defaultPrevented).toBe(true);
expect(e.cancelBubble).toBe(true);
}
},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1471,7 +1604,7 @@ describe("t-on", () => {
},
onClickSelf(e) {
steps.push("onClickSelf");
}
},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1495,10 +1628,10 @@ describe("t-on", () => {
);
let steps: boolean[] = [];
let owner = {
onClick() {}
onClick() {},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
(<HTMLElement>node).addEventListener("click", function(e) {
(<HTMLElement>node).addEventListener("click", function (e) {
steps.push(e.defaultPrevented);
});
@@ -1520,10 +1653,10 @@ describe("t-on", () => {
);
let steps: boolean[] = [];
let owner = {
onClick() {}
onClick() {},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
(<HTMLElement>node).addEventListener("click", function(e) {
(<HTMLElement>node).addEventListener("click", function (e) {
steps.push(e.defaultPrevented);
});
@@ -1551,13 +1684,13 @@ describe("t-on", () => {
const owner = {
projects: [
{ id: 1, name: "Project 1" },
{ id: 2, name: "Project 2" }
{ id: 2, name: "Project 2" },
],
onEdit(projectId, ev) {
expect(ev.defaultPrevented).toBe(true);
steps.push(projectId);
}
},
};
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1582,7 +1715,7 @@ describe("t-on", () => {
);
const node = renderToDOM(qweb, "test", {}, { handlers: [] });
node.addEventListener("click", e => {
node.addEventListener("click", (e) => {
expect(e.defaultPrevented).toBe(true);
});
@@ -1598,7 +1731,7 @@ describe("t-on", () => {
text: "Click here",
onClick() {
steps.push("onClick");
}
},
};
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1617,7 +1750,7 @@ describe("t-on", () => {
html: "Click <b>here</b>",
onClick() {
steps.push("onClick");
}
},
};
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1644,7 +1777,7 @@ describe("t-on", () => {
},
doSomething() {
steps.push("normal");
}
},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1717,7 +1850,7 @@ describe("loading templates", () => {
});
});
describe("special cases for some boolean html attributes/properties", () => {
describe("special cases for some specific html attributes/properties", () => {
test("input type= checkbox, with t-att-checked", () => {
qweb.addTemplate("test", `<input type="checkbox" t-att-checked="flag"/>`);
const result = renderToString(qweb, "test", { flag: true });
@@ -1744,6 +1877,33 @@ describe("special cases for some boolean html attributes/properties", () => {
);
renderToString(qweb, "test", { flag: true });
});
test("input with t-att-value", () => {
// render input with initial value
qweb.addTemplate("test", `<input t-att-value="v"/>`);
const vnode1 = qweb.render("test", { v: "zucchini" });
const vnode2 = patch(document.createElement("input"), vnode1);
let elm = vnode2.elm as HTMLInputElement;
expect(elm.value).toBe("zucchini");
// change value manually in input, to simulate user input
elm.value = "tomato";
expect(elm.value).toBe("tomato");
// rerender with a different value, and patch actual dom, to check that
// input value was properly reset by owl
const vnode3 = qweb.render("test", { v: "potato" });
patch(vnode2, vnode3);
expect(elm.value).toBe("potato");
});
test("input of type checkbox with t-att-indeterminate", () => {
qweb.addTemplate("test", `<input type="checkbox" t-att-indeterminate="v"/>`);
const vnode1 = qweb.render("test", { v: true });
const vnode2 = patch(document.createElement("input"), vnode1);
let elm = vnode2.elm as HTMLInputElement;
expect(elm.indeterminate).toBe(true);
});
});
describe("whitespace handling", () => {
@@ -1808,7 +1968,7 @@ describe("t-key", () => {
);
expect(
renderToString(qweb, "test", {
beers: [{ id: 12, name: "Chimay Rouge" }]
beers: [{ id: 12, name: "Chimay Rouge" }],
})
).toMatchSnapshot();
});
@@ -1921,9 +2081,9 @@ describe("properly support svg", () => {
describe("translation support", () => {
test("can translate node content", () => {
const translations = {
word: "mot"
word: "mot",
};
const translateFn = expr => translations[expr] || expr;
const translateFn = (expr) => translations[expr] || expr;
const qweb = new QWeb({ translateFn });
qweb.addTemplate("test", "<div>word</div>");
expect(renderToString(qweb, "test")).toBe("<div>mot</div>");
@@ -1931,9 +2091,9 @@ describe("translation support", () => {
test("does not translate node content if disabled", () => {
const translations = {
word: "mot"
word: "mot",
};
const translateFn = expr => translations[expr] || expr;
const translateFn = (expr) => translations[expr] || expr;
const qweb = new QWeb({ translateFn });
qweb.addTemplate(
"test",
@@ -1948,9 +2108,9 @@ describe("translation support", () => {
test("some attributes are translated", () => {
const translations = {
word: "mot"
word: "mot",
};
const translateFn = expr => translations[expr] || expr;
const translateFn = (expr) => translations[expr] || expr;
const qweb = new QWeb({ translateFn });
qweb.addTemplate(
"test",
+10 -10
View File
@@ -6,12 +6,12 @@ describe("tokenizer", () => {
expect(tokenize("{}")).toEqual([
{ type: "LEFT_BRACE", value: "{" },
{ type: "RIGHT_BRACE", value: "}" }
{ type: "RIGHT_BRACE", value: "}" },
]);
expect(tokenize("{ }}")).toEqual([
{ type: "LEFT_BRACE", value: "{" },
{ type: "RIGHT_BRACE", value: "}" },
{ type: "RIGHT_BRACE", value: "}" }
{ type: "RIGHT_BRACE", value: "}" },
]);
expect(tokenize("a")).toEqual([{ type: "SYMBOL", value: "a" }]);
expect(tokenize("true")).toEqual([{ type: "SYMBOL", value: "true" }]);
@@ -25,15 +25,15 @@ describe("tokenizer", () => {
{ type: "SYMBOL", value: "a" },
{ type: "COLON", value: ":" },
{ type: "VALUE", value: "2" },
{ type: "RIGHT_BRACE", value: "}" }
{ type: "RIGHT_BRACE", value: "}" },
]);
expect(tokenize("a,")).toEqual([
{ type: "SYMBOL", value: "a" },
{ type: "COMMA", value: "," }
{ type: "COMMA", value: "," },
]);
expect(tokenize("][")).toEqual([
{ 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: "!=" },
]);
expect(tokenize("typeof a")).toEqual([
{ type: "OPERATOR", value: "typeof " },
{ type: "SYMBOL", value: "a" }
{ type: "SYMBOL", value: "a" },
]);
expect(tokenize("a...1")).toEqual([
{ type: "SYMBOL", value: "a" },
{ type: "OPERATOR", value: "..." },
{ type: "VALUE", value: "1" }
{ type: "VALUE", value: "1" },
]);
expect(tokenize("a in b")).toEqual([
{ type: "SYMBOL", value: "a" },
{ 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"' },
{ type: "RIGHT_BRACE", value: "}" }
{ type: "RIGHT_BRACE", value: "}" },
]);
expect(tokenize('"hello \\"ged\\""')).toEqual([{ type: "VALUE", value: '"hello \\"ged\\""' }]);
});
+3 -3
View File
@@ -37,7 +37,7 @@ describe("Link component", () => {
const routes = [
{ name: "about", path: "/about" },
{ name: "users", path: "/users" }
{ name: "users", path: "/users" },
];
router = new TestRouter(env, routes, { mode: "history" });
@@ -71,7 +71,7 @@ describe("Link component", () => {
const routes = [
{ name: "about", path: "/about" },
{ name: "users", path: "/users" }
{ name: "users", path: "/users" },
];
router = new TestRouter(env, routes, { mode: "history" });
@@ -81,7 +81,7 @@ describe("Link component", () => {
expect(window.location.pathname).toBe("/users");
var evt = new MouseEvent("click", {
button: 1
button: 1,
});
fixture.querySelector("a")!.dispatchEvent(evt);
+1 -1
View File
@@ -41,7 +41,7 @@ describe("RouteComponent", () => {
const routes = [
{ 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" });
+10 -10
View File
@@ -21,7 +21,7 @@ describe("router miscellaneous", () => {
test("validate routes shape", () => {
expect(() => {
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"}`);
});
@@ -133,7 +133,7 @@ describe("getRouteParams", () => {
test("match some parameterized routes", () => {
router = new TestRouter(env, []);
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" });
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "#/invoices/3")).toEqual(
{
id: "3"
id: "3",
}
);
});
@@ -151,7 +151,7 @@ describe("getRouteParams", () => {
expect(
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "/invoices/3")
).toEqual({
id: 3
id: 3,
});
});
@@ -160,7 +160,7 @@ describe("getRouteParams", () => {
expect(
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "#/invoices/3")
).toEqual({
id: 3
id: 3,
});
});
});
@@ -169,7 +169,7 @@ describe("redirect", () => {
test("can redirect to other route", async () => {
router = new TestRouter(env, [
{ 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();
@@ -182,7 +182,7 @@ describe("redirect", () => {
test("can redirect to other path", async () => {
router = new TestRouter(env, [
{ 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);
@@ -214,7 +214,7 @@ describe("beforeRouteEnter", () => {
const guard = jest.fn(() => false);
router = new TestRouter(env, [
{ name: "routea", path: "/some/patha" },
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard }
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard },
]);
await router.start();
@@ -235,7 +235,7 @@ describe("beforeRouteEnter", () => {
router = new TestRouter(env, [
{ name: "routea", path: "/some/patha" },
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard },
{ name: "routec", path: "/some/pathc" }
{ name: "routec", path: "/some/pathc" },
]);
await router.start();
@@ -255,7 +255,7 @@ describe("beforeRouteEnter", () => {
});
router = new TestRouter(env, [
{ name: "landing", path: "/", beforeRouteEnter: guard },
{ name: "otherroute", path: "/some/other/route" }
{ name: "otherroute", path: "/some/other/route" },
]);
expect(window.location.pathname).toBe("/");
+28 -28
View File
@@ -8,7 +8,7 @@ describe("basic use", () => {
const actions = {
inc({ state }, delta) {
state.n += delta;
}
},
};
const store = new Store({ state, actions });
@@ -25,7 +25,7 @@ describe("basic use", () => {
state.n1 += delta1;
state.n2 += delta2;
state.n3 += delta3;
}
},
};
const store = new Store({ state, actions });
@@ -47,7 +47,7 @@ describe("basic use", () => {
},
inc100({ dispatch }) {
dispatch("inc", 100);
}
},
};
const store = new Store({ state, actions });
@@ -61,7 +61,7 @@ describe("basic use", () => {
const actions = {
inc({ state }) {
return ++state.n;
}
},
};
const store = new Store({ state, actions });
@@ -87,7 +87,7 @@ describe("basic use", () => {
async setTo10({ dispatch }) {
await Promise.resolve();
dispatch("setN", 10);
}
},
};
const store = new Store({ state, actions });
@@ -115,7 +115,7 @@ describe("basic use", () => {
await Promise.resolve();
dispatch("setN", 10);
return 5;
}
},
};
const store = new Store({ state, actions });
@@ -132,7 +132,7 @@ describe("basic use", () => {
const actions = {
someaction({ env }) {
expect(env).toBe(someEnv);
}
},
};
const store = new Store({ state: {}, actions, env: someEnv });
@@ -145,15 +145,15 @@ describe("basic use", () => {
1: {
id: 1,
name: "bertinchamps",
tasterID: 1
}
tasterID: 1,
},
},
tasters: {
1: {
id: 1,
name: "aaron"
}
}
name: "aaron",
},
},
};
const getters: { [key: string]: Getter } = {
beerTasterName({ state }, beerID) {
@@ -161,7 +161,7 @@ describe("basic use", () => {
},
bestBeerName({ state }) {
return state.beers[1].name;
}
},
};
const store = new Store({ state, actions: {}, getters });
expect(store.getters).toBeDefined();
@@ -176,15 +176,15 @@ describe("basic use", () => {
1: {
id: 1,
name: "bertinchamps",
tasterID: 1
}
tasterID: 1,
},
},
tasters: {
1: {
id: 1,
name: "aaron"
}
}
name: "aaron",
},
},
};
const getters = {
beerTasterName({ state }, beerID) {
@@ -192,14 +192,14 @@ describe("basic use", () => {
},
bestBeerName({ state }) {
return state.beers[1].name;
}
},
};
const actions = {
action({ getters }) {
expect(getters).toBeDefined();
expect(getters.bestBeerName()).toBe("bertinchamps");
expect(getters.beerTasterName(1)).toBe("aaron");
}
},
};
const store = new Store({ state, actions, getters });
store.dispatch("action");
@@ -215,7 +215,7 @@ describe("basic use", () => {
},
c({}, i) {
return `c${i}`;
}
},
};
const store = new Store({ getters, state: {}, actions: {} });
@@ -226,7 +226,7 @@ describe("basic use", () => {
describe("advanced state properties", () => {
test("state in the store is reference equal after mutation", async () => {
const actions = {
donothing() {}
donothing() {},
};
const store = new Store({ state: {}, actions });
const state = store.state;
@@ -242,7 +242,7 @@ describe("advanced state properties", () => {
expect(state.a.length).toBe(3);
const l = state.a.push(53);
expect(l).toBe(4);
}
},
};
const store = new Store({ state, actions });
store.dispatch("m");
@@ -253,11 +253,11 @@ describe("advanced state properties", () => {
const actions = {
dosomething({ state }) {
Object.assign(state.westmalle, { a: 3, b: 4 });
}
},
};
const store = new Store({
state: { westmalle: { a: 1, b: 2 } },
actions
actions,
});
store.dispatch("dosomething");
expect(store.state.westmalle).toEqual({ a: 3, b: 4 });
@@ -267,7 +267,7 @@ describe("advanced state properties", () => {
const actions = {
inc({ state }) {
state.counter++;
}
},
};
const state = { counter: 0 };
const store = new Store({ state, actions });
@@ -284,7 +284,7 @@ describe("updates triggered by the store", () => {
const actions = {
inc({ state }, delta) {
state.n += delta;
}
},
};
const store = new Store({ state, actions });
store.on("update", null, () => updateCounter++);
@@ -308,7 +308,7 @@ describe("updates triggered by the store", () => {
noop2({ state }) {
const val = state.n;
state.n = val;
}
},
};
const store = new Store({ state, actions });
store.on("update", null, () => updateCounter++);
+93 -93
View File
@@ -1,5 +1,5 @@
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 { xml } from "../src/tags";
import { shallowEqual } from "../src/utils";
@@ -25,7 +25,7 @@ describe("connecting a component to store", () => {
const actions = {
addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ });
}
},
};
const store = new Store({ state, actions });
@@ -34,7 +34,7 @@ describe("connecting a component to store", () => {
<div>
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
</div>`;
todos = useStore(state => state.todos);
todos = useStore((state) => state.todos);
}
(<any>env).store = store;
@@ -56,7 +56,7 @@ describe("connecting a component to store", () => {
},
setNotNull({ state }) {
state.nullValue = "ok";
}
},
};
const store = new Store({ state, actions });
@@ -70,15 +70,15 @@ describe("connecting a component to store", () => {
nullValue: string;
constructor() {
super();
this.isBoolean = useStore(state => state.isBoolean, {
onUpdate: isBoolean => {
this.isBoolean = useStore((state) => state.isBoolean, {
onUpdate: (isBoolean) => {
this.isBoolean = isBoolean;
}
},
});
this.nullValue = useStore(state => state.nullValue, {
onUpdate: nullValue => {
this.nullValue = useStore((state) => state.nullValue, {
onUpdate: (nullValue) => {
this.nullValue = nullValue;
}
},
});
}
}
@@ -105,15 +105,15 @@ describe("connecting a component to store", () => {
class App extends Component {
static template = xml`<div t-esc="mapAdd"/>`;
storeProps = {
array: useStore(state => {
array: useStore((state) => {
if (state.useSmallArray) {
return state.smallerArray;
}
return state.biggerArray;
})
}),
};
get mapAdd() {
return this.storeProps.array.map(a => {
return this.storeProps.array.map((a) => {
return a + 1;
});
}
@@ -133,7 +133,7 @@ describe("connecting a component to store", () => {
test("throw error if no store is found", async () => {
class App extends Component {
static template = xml`<div></div>`;
todos = useStore(state => state.todos);
todos = useStore((state) => state.todos);
}
let error;
@@ -153,7 +153,7 @@ describe("connecting a component to store", () => {
class App extends Component {
static template = xml`<div/>`;
storeState = useStore(state => state.a);
storeState = useStore((state) => state.a);
}
(<any>env).store = store;
@@ -170,7 +170,7 @@ describe("connecting a component to store", () => {
doSomething({ state }) {
state.a = 2;
state.b = 3;
}
},
};
const store = new Store({ state, actions });
@@ -180,8 +180,8 @@ describe("connecting a component to store", () => {
<span t-esc="a.value"/>
<span t-esc="b.value"/>
</div>`;
a = useStore(state => ({ value: state.a }));
b = useStore(state => ({ value: state.b }));
a = useStore((state) => ({ value: state.a }));
b = useStore((state) => ({ value: state.b }));
}
App.prototype.__render = jest.fn(App.prototype.__render);
@@ -204,7 +204,7 @@ describe("connecting a component to store", () => {
const actions = {
addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ });
}
},
};
const store = new Store({ state, actions });
@@ -213,7 +213,7 @@ describe("connecting a component to store", () => {
<div>
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
</div>`;
todos = useStore(state => state.todos);
todos = useStore((state) => state.todos);
}
App.prototype.__render = jest.fn(App.prototype.__render);
@@ -241,13 +241,13 @@ describe("connecting a component to store", () => {
const actions = {
addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ });
}
},
};
const store = new Store({ state, actions });
class App extends Component {
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;
@@ -270,12 +270,12 @@ describe("connecting a component to store", () => {
},
incrementA({ state }) {
state.a++;
}
},
};
const store = new Store({ state, actions });
class App extends Component {
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);
@@ -303,7 +303,7 @@ describe("connecting a component to store", () => {
const actions = {
addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ });
}
},
};
const store = new Store({ state, actions });
@@ -312,7 +312,7 @@ describe("connecting a component to store", () => {
<div>
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
</div>`;
todos = useStore(state => state.todos, { store });
todos = useStore((state) => state.todos, { store });
}
const app = new App();
@@ -331,8 +331,8 @@ describe("connecting a component to store", () => {
actions: {
inc({ state }) {
state.value++;
}
}
},
},
});
(<any>env).store = store;
@@ -342,7 +342,7 @@ describe("connecting a component to store", () => {
<button t-on-click="dispatch('inc')">Inc</button>
<span><t t-esc="storeState.value"/></span>
</div>`;
storeState = useStore(state => state);
storeState = useStore((state) => state);
dispatch = useDispatch();
}
const app = new App();
@@ -360,15 +360,15 @@ describe("connecting a component to store", () => {
const state = {
todos: [
{ id: 1, text: "jupiler" },
{ id: 2, text: "chimay" }
]
{ id: 2, text: "chimay" },
],
};
const store = new Store({ state, actions: {} });
class TodoItem extends Component {
static template = xml`<span><t t-esc="todo.text"/></span>`;
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 = {
addTodo({ state }, text) {
state.todos.push({ text, id: nextId++ });
}
},
};
const store = new Store({ state, actions });
class TodoItem extends Component {
static template = xml`<span><t t-esc="todo.text"/></span>`;
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"/>
</div>`;
static components = { TodoItem };
todos = useStore(state => state.todos);
todos = useStore((state) => state.todos);
}
(<any>env).store = store;
@@ -431,16 +431,16 @@ describe("connecting a component to store", () => {
importantID: 1,
todos: [
{ id: 1, text: "jupiler" },
{ id: 2, text: "bertinchamps" }
]
{ id: 2, text: "bertinchamps" },
],
};
const getters = {
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) {
return state.todos.find(todo => todo.id === id).text;
}
return state.todos.find((todo) => todo.id === id).text;
},
};
const store = new Store({ state, getters });
@@ -452,10 +452,10 @@ describe("connecting a component to store", () => {
</div>`;
getters = useGetters();
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 {
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"/>
</t>
</div>`;
todos = useStore(state => state.todos);
todos = useStore((state) => state.todos);
}
(<any>env).store = store;
@@ -513,13 +513,13 @@ describe("connecting a component to store", () => {
</div>`;
// we have here a new object
data = useStore(state => ({ beers: state.beers, otherKey: 1 }));
data = useStore((state) => ({ beers: state.beers, otherKey: 1 }));
}
const actions = {
addBeer({ state }, name) {
state.beers.push({ name });
}
},
};
const state = { beers: [{ name: "jupiler" }] };
@@ -542,9 +542,9 @@ describe("connecting a component to store", () => {
class Beer extends Component {
static template = xml`<span><t t-esc="beer.name"/></span>`;
beer = useStore((state, props) => state.beers[props.id], {
onUpdate: result => {
onUpdate: (result) => {
++counter;
}
},
});
}
@@ -558,7 +558,7 @@ describe("connecting a component to store", () => {
const actions = {
renameBeer({ state }, { id, name }) {
state.beers[id].name = name;
}
},
};
const store = new Store({ state, actions });
(<any>env).store = store;
@@ -609,7 +609,7 @@ describe("connecting a component to store", () => {
data = useStore((state, props) => ({
selected: state.beers[props.id],
consumed: state.beers[state.consumedID] || null,
taster: state.taster
taster: state.taster,
}));
}
@@ -622,14 +622,14 @@ describe("connecting a component to store", () => {
const actions = {
consume({ state }, beerId) {
state.consumedID = beerId;
}
},
};
const state = {
beers: {
1: { name: "jupiler" }
1: { name: "jupiler" },
},
consumedID: null,
taster: "aaron"
taster: "aaron",
};
const store = new Store({ state, actions });
(<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.consumed">consumed:<t t-esc="info.consumed.name"/></span>
</div>`;
info = useStore(function(state, props) {
info = useStore(function (state, props) {
return {
selected: state.beers[props.id],
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 }) {
state.beers[beerId].name = name;
}
},
};
const state = {
beers: {
1: { name: "jupiler" }
1: { name: "jupiler" },
},
consumedID: null,
taster: "aaron"
taster: "aaron",
};
const store = new Store({ state, actions });
(<any>env).store = store;
@@ -747,11 +747,11 @@ describe("connecting a component to store", () => {
const actions = {
setValue({ state }, val) {
state.x.val = val;
}
},
};
class Parent extends Component {
static template = xml`<div><t t-esc="x.val"/></div>`;
x = useStore(state => {
x = useStore((state) => {
return Object.assign({}, state.x);
});
dispatch = useDispatch();
@@ -782,11 +782,11 @@ describe("connecting a component to store", () => {
static template = xml`<div><Child key="state.current"/></div>`;
static components = { Child };
state = useStore(state => {
state = useStore((state) => {
steps.push("parent");
return {
current: state.current,
isvisible: state.isvisible
isvisible: state.isvisible,
};
});
}
@@ -795,7 +795,7 @@ describe("connecting a component to store", () => {
const actions = {
setCurrent({ state }, c) {
state.current = c;
}
},
};
const store = new Store({ state, actions });
@@ -831,7 +831,7 @@ describe("connecting a component to store", () => {
<Child t-if="state.flag" someId="state.someId"/>
</div>`;
static components = { Child };
state = useStore(s => {
state = useStore((s) => {
steps.push("parent");
return { flag: s.flag, someId: s.someId };
});
@@ -846,7 +846,7 @@ describe("connecting a component to store", () => {
const actions = {
setFlagToFalse({ state }) {
state.flag = false;
}
},
};
const store = new Store({ state, actions });
@@ -874,18 +874,18 @@ describe("connecting a component to store", () => {
const actions = {
editTodo({ state }) {
state.todos[1].title = "abc";
}
},
};
const todos = { 1: { id: 1, title: "kikoou" } };
const state = {
todos
todos,
};
const store = new Store({
state,
actions
actions,
});
class TodoItem extends Component {
class TodoItem extends Component<{}, EnvWithStore> {
static template = xml`
<div class="todo">
<t t-esc="state.todo.title"/>
@@ -894,7 +894,7 @@ describe("connecting a component to store", () => {
state = useStore((state, props) => {
steps.push("item:usestore");
return {
todo: state.todos[props.id]
todo: state.todos[props.id],
};
});
@@ -914,7 +914,7 @@ describe("connecting a component to store", () => {
</t>
</div>`;
static components = { TodoItem };
state = useStore(state => {
state = useStore((state) => {
steps.push("app:usestore");
return { todos: state.todos };
});
@@ -947,18 +947,18 @@ describe("connecting a component to store", () => {
const actions = {
removeTodo({ state }) {
delete state.todos[1];
}
},
};
const todos = { 1: { id: 1, title: "kikoou" } };
const state = {
todos
todos,
};
const store = new Store({
state,
actions
actions,
});
class TodoItem extends Component {
class TodoItem extends Component<{}, EnvWithStore> {
static template = xml`
<div class="todo">
<t t-esc="state.todo.title"/>
@@ -967,7 +967,7 @@ describe("connecting a component to store", () => {
state = useStore((state, props) => {
steps.push("item:usestore");
return {
todo: state.todos[props.id]
todo: state.todos[props.id],
};
});
@@ -988,7 +988,7 @@ describe("connecting a component to store", () => {
</t>
</div>`;
static components = { TodoItem };
state = useStore(state => {
state = useStore((state) => {
steps.push("app:usestore");
return { todos: state.todos };
});
@@ -1016,7 +1016,7 @@ describe("connecting a component to store", () => {
"item:usestore",
"item:render",
"app:usestore",
"app:render"
"app:render",
]);
expect(fixture.innerHTML).toBe('<div class="todoapp"></div>');
});
@@ -1026,7 +1026,7 @@ describe("connecting a component to store", () => {
class App extends Component {
static template = xml`<div><t t-esc="store.msg"/></div>`;
store = useStore(s => ({ msg: s.msg }));
store = useStore((s) => ({ msg: s.msg }));
willPatch() {
steps.push("willpatch");
@@ -1040,7 +1040,7 @@ describe("connecting a component to store", () => {
const actions = {
setMsg({ state }, c) {
state.msg = c;
}
},
};
const store = new Store({ state, actions });
@@ -1061,7 +1061,7 @@ describe("connecting a component to store", () => {
class Child extends Component {
static template = xml`<div><t t-esc="store.val"/></div>`;
store = useStore(s => s);
store = useStore((s) => s);
}
class Parent extends Component {
@@ -1099,7 +1099,7 @@ describe("connecting a component to store", () => {
class Child extends Component {
static template = xml`<div><t t-esc="store.val"/></div>`;
store = useStore(s => {
store = useStore((s) => {
steps.push("child selector");
return s;
});
@@ -1107,7 +1107,7 @@ describe("connecting a component to store", () => {
class Parent extends Component {
static template = xml`<div><Child t-if="store.child" /></div>`;
static components = { Child };
store = useStore(s => {
store = useStore((s) => {
steps.push("parent selector");
return s;
});
@@ -1118,7 +1118,7 @@ describe("connecting a component to store", () => {
const actions = {
toggleChild({ state }) {
state.child = !state.child;
}
},
};
const store = new Store({ state, actions });
(<any>env).store = store;
@@ -1137,18 +1137,18 @@ describe("connecting a component to store", () => {
test("dispatch an action", async () => {
class App extends Component {
static template = xml`<div><t t-esc="store.counter"/></div>`;
store = useStore(state => state);
store = useStore((state) => state);
dispatch = useDispatch();
}
const state = {
counter: 0
counter: 0,
};
const actions = {
inc({ state }) {
return ++state.counter;
}
},
};
const store = new Store({ state, actions });
@@ -1185,21 +1185,21 @@ describe("various scenarios", () => {
await Promise.resolve();
delete state.attachments[100];
state.messages[10].attachmentIds = [];
}
},
};
const state = {
attachments: {
100: {
id: 100,
name: "text.txt"
}
name: "text.txt",
},
},
messages: {
10: {
attachmentIds: [100],
id: 10
}
}
id: 10,
},
},
};
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"/>
</div>`;
store = useStore(state => ({ attachmentIds: state.messages[10].attachmentIds }));
store = useStore((state) => ({ attachmentIds: state.messages[10].attachmentIds }));
static components = { Attachment };
state = { isAttachmentDeleted: false };
dispatch = useDispatch();
+2 -2
View File
@@ -20,7 +20,7 @@ debugOwl(owl, {});
test("can log full lifecycle", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
console.log = (arg) => steps.push(arg);
class Child extends Component {
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] Parent<id=1> willPatch",
"[OWL_DEBUG] Child<id=2> mounted",
"[OWL_DEBUG] Parent<id=1> patched"
"[OWL_DEBUG] Parent<id=1> patched",
]);
console.log = log;
});
+2 -2
View File
@@ -19,7 +19,7 @@ debugOwl(owl, { logScheduler: true });
test("can log scheduler start and stop", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
console.log = (arg) => steps.push(arg);
class Child extends Component {
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> mounted",
"[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue"
"[OWL_DEBUG] scheduler: stop running tasks queue",
]);
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 () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
console.log = (arg) => steps.push(arg);
class Parent extends Component {
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] scheduler: stop running tasks queue",
"[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;
});
+2 -2
View File
@@ -19,7 +19,7 @@ debugOwl(owl, { logScheduler: true });
test("log a sub component with non stringifiable props", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
console.log = (arg) => steps.push(arg);
class Child extends Component {
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] scheduler: stop running tasks queue",
"[OWL_DEBUG] Parent<id=1> willUnmount",
"[OWL_DEBUG] Child<id=2> willUnmount"
"[OWL_DEBUG] Child<id=2> willUnmount",
]);
console.log = log;
});
+7 -7
View File
@@ -60,14 +60,14 @@ function getFiles(path: string[] = []): FileData[] {
if (path.length === 0) {
const baseFiles: FileData[] = [
{ 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 result = baseFiles.concat(rest);
result.forEach(addMardownData);
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()) {
return getFiles(path.concat(f.name));
}
@@ -78,8 +78,8 @@ function getFiles(path: string[] = []): FileData[] {
path,
links: [],
sections: [],
fullName
}
fullName,
},
];
});
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
let target: FileData | undefined = files.find(f => f.fullName === linkFullName);
let target: FileData | undefined = files.find((f) => f.fullName === linkFullName);
if (!target) {
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
// link name
if (hash) {
if (!target.sections.find(s => s.slug === hash)) {
if (!target.sections.find((s) => s.slug === hash)) {
return false;
}
}
@@ -153,7 +153,7 @@ function slugify(str) {
.toLowerCase()
.replace(/\//g, "") // remove /
.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(/[^\w\-]+/g, "") // Remove all non-word characters
.replace(/\-\-+/g, "-") // Replace multiple - with single -
+5
View File
@@ -1,4 +1,5 @@
import { escape, debounce } from "../src/utils";
import { browser } from "../src/browser";
describe("escape", () => {
test("normal strings", () => {
@@ -15,6 +16,10 @@ describe("escape", () => {
describe("debounce", () => {
test("works as expected", () => {
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 f = debounce(() => n++, 100);
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.
// 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).
// 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
// 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">
<Card title="'Title card A'">
<t t-set="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="content">Content of card 1... [<t t-esc="state.a"/>]</t>
<t t-set-slot="footer"><button t-on-click="inc('a', 1)">Increment A</button></t>
</Card>
<Card title="'Title card B'">
<t t-set="content">
<t t-set-slot="content">
<div>Card 2... [<t t-esc="state.b"/>]</div>
<Counter />
</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>
</div>
</templates>`;
-1
View File
@@ -106,7 +106,6 @@ async function startRelease() {
draft: true // todo: remove this someday
};
const result = await createRelease(token, options);
console.log(result);
await ask("Ready for next step...");
// ---------------------------------------------------------------------------