Compare commits

..

54 Commits

Author SHA1 Message Date
Simon Genin (ges) 9e2202c02a [IMP] devtools: add __owl__ access.
To be able to make devtools, we need access some inner state of a
component. We create a __owl_devtools__ variable on components.

Just like in Vue js, we attach it to the HTMLElement of a component.
It's how we give it to the "outside world", through the DOM.

For its update, we use the onMounted and onPatched hook.
2020-11-10 17:22:37 +01:00
Stephen Mugisha c8a27aa1a3 Fix minor grammatical errors
Correct minor grammatical mistakes in the `why_owl.md` file
2020-11-02 16:36:16 +01:00
Simon Genin 323cb61d2c [DOC] minor fix - missing this.
Proposed by @obayit with https://github.com/odoo/owl/pull/689
2020-11-02 16:33:02 +01:00
glovebx ac9cc91701 typo correct 2020-11-02 16:03:10 +01:00
Géry Debongnie d83cfc12ee [IMP] qweb: the t-slot directive is now dynamic
It is useful for some kind of components to be able to use a completely
dynamic slot expression.
2020-10-30 15:31:03 +01:00
Géry Debongnie cb07c99d40 [IMP] owl: add a new mount method 2020-10-30 15:31:03 +01:00
Géry Debongnie d615ffd81b [REL] v1.0.13
# Owl v1.0.13

## Bug fixes

- fix: slots: prevent infinite loop in some cases
2020-10-26 08:32:05 +01:00
Géry Debongnie 8d2b250fef [FIX] slots: prevent infinite loop in some cases
The reviewer for the commit on named slots inside named slots did not
notice that there was an infinite loop.  Because of his sloppiness, Owl
could block in an infinite loop when a named t-slots was defined inside
a subcomponent, but not as a direct child.
2020-10-23 12:14:46 +02:00
aab-odoo 7626cc01b3 [IMP] rollup: properly indent generated files (#774)
Also lint rollup.config.js
2020-10-22 13:58:10 +02:00
Géry Debongnie 392185ab67 [REL] v1.0.12
# Owl v1.0.12

## Bug fixes

- fix: properly handle named t-slots inside named t-slots
- fix: do not render comments as text in t-raw
- fix: allow dynamic template in t-call
- fix: correctly handle top-level t-call with body
- fix: correct cause of non determinism in tests

## Tooling improvements

- setup github actions for CI
- improve owl bundling pipeline
- add  flag in owl configuration
- add prettier to github CI
2020-10-20 17:35:23 +02:00
Géry Debongnie 392dc0131a [FIX] config: make enableTransitions work on components (#773)
Fun: the transition is handled at two different places, once for dom
nodes, once for components.  Obviously, I only applied the change to the
first case and forgot about the second.
2020-10-20 17:30:08 +02:00
Simon Genin 4e8e9dee3e Update deploy.yml => add prettier (#772)
* Update deploy.yml

Add the npm run prettier command action to every push request

* Update deploy.yml
2020-10-20 16:38:14 +02:00
Simon Genin 25d4cb2aab Improve the release script for easier deployment (#771)
Co-authored-by: Simon Genin (ges) <ges@odoo.com>
2020-10-20 16:36:14 +02:00
Bruno Boi 4724652533 [IMP] component: allow using TypeScript type hints (#758) 2020-10-20 16:30:09 +02:00
Géry Debongnie b08cc1d084 [IMP] config: add enableTransitions flag to config
It is useful to be able to disable the t-transition directive for
testing purposes.  This commit introduces a global config flag to do
just that.

closes #768
2020-10-20 13:22:25 +02:00
Géry Debongnie a68d7774c6 [FIX] qweb: issue when rendering twice dynamic t-calls
The previous implementation generated an id each time the template was
rendered, instead of just once per new dynamic template found.
2020-10-20 08:02:03 +02:00
Géry Debongnie ed9d820d9e [FIX] tests: fix cause of non determinism
Since commit 7e4baf6, some tests were failing non deterministically. It
seems like the root cause for the problem is that the nextTick method
was sometimes completed too early.

From my understanding of the problem, the nextTick method was correct,
however it is running in a jest test, in node, which simulates the
requestanimationframe behaviour, probably not perfectly.

Switching to an alternate implementation, which swaps the setTimeout and
the requestanimationframe seems to be more solid.

Note that this commit had to introduce the nextFrame method as well,
because the animation tests really seems to want to be in the next
frame, not in a callback executed in the same frame, but at the end.

closes #757
2020-10-19 21:43:49 +02:00
Géry Debongnie 4a2d019161 [DOC] qweb: explain how to work with iterables in t-foreach
Owl is meant to stay low level-ish, and making it work magically with
sets/maps/... seems to be a little too much in term of code/magic. Also,
using iterables in t-foreach is actually quite simple, making this
improvement not as important.

closes #720
closes #754
2020-10-19 20:52:31 +02:00
Géry Debongnie 89eae191b1 [FIX] qweb: correctly handle top level t-call and body
Whenever a top level t-call was made with some non empty body, Owl
complained that a template should not have more than one root node.

The reason was that the compilation context for the body of the t-call
directive was the same as the root compilation context, and it already
had a parentNode set.

To fix the issue, this commit simply use the subContext method to create
a different compilation context, which actually makes sense, because the
body of a t-call is really a different situation.  Also, as a bonus, it
slightly improves the code for the t-call directive.

closes #760
2020-10-19 20:51:44 +02:00
Géry Debongnie 3fbc02986c [FIX] qweb: allow string interpolation in t-call
This commit allows the t-call directive to choose a dynamic template.
This is working in QWeb Python, but was not possible in Owl.

closes #709
2020-10-19 16:27:38 +02:00
Géry Debongnie a28ce440dc [REF] qweb: simplify _compile method arguments
Strangely, the _compile method required a CompilationContext whenever it
was compiled as a sub template, but this parent context was actually not
really needed.  I guess that it was the case in the past, but this was
changed at some point.

This commit makes another significant change: the xml element is no
longer mandatory.  It is actually only required for slots (because the
template is not registered to qweb).

Finally, the interface for the whole method has been changed to use an
option object, which makes more sense with 3 optional paremeters.
2020-10-19 16:27:38 +02:00
Géry Debongnie 8e018fbbda [FIX] qweb: do not render comments as text in t-raw
The code converting html to a vnode did not handle comment nodes as
expected.

closes #761
2020-10-19 16:27:14 +02:00
Simon Genin 33f7fc8e83 [IMP] bundling: Improve owl bundling process and output (#751)
The bundle pipeline can use a bit of a improvement:

Most of the process is moved from npm scripts to rollup.
Add package keys to smooth out the use of bundlers of the end users.
(Rollup by example check amongst others the main, module and browser key
to import the right version of a lib when needed).
Refactor rollup logic to make it more modular.
Add bundled module formats.
Add comments to tsconfig to see more clearly what's possible.
Add reference tag to help @types discovery in test files.

Co-authored-by: Simon Genin (ges) <ges@odoo.com>
2020-10-16 16:11:11 +02:00
Simon Genin 6a833c54ec [IMP] setup github actions for CI
Add action to launch the tests on the PRs
2020-10-13 16:57:24 +02:00
Géry Debongnie cb38d795f9 [FIX] slots: properly handle named t-slots inside named t-slots
Previous code naively handled nested t-set-slots: if a second named
slots was found, it overrode the first.

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

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

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

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

Bug fixes!

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

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

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

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

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

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

It is then interpreted as:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

<input value="abc"/>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

But the code did not work in the other direction:

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

With this commit, we just add the missing if statement

closes #664
2020-04-21 16:23:31 +02:00
Géry Debongnie 23ce19e57a [REL] v1.0.7 2020-04-17 15:52:52 +02:00
Géry Debongnie aaf40e9bec [IMP] tooling: add iife build to npm package
This is useful to link to the iife version in some other projects.
2020-04-17 15:47:20 +02:00
88 changed files with 3202 additions and 1492 deletions
+27
View File
@@ -0,0 +1,27 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run test
- run: npm run prettier
+2
View File
@@ -27,3 +27,5 @@ node_modules
/tools/owl.js /tools/owl.js
release-notes.md release-notes.md
.rpt2_cache
+3 -5
View File
@@ -41,7 +41,7 @@ find some more additional information [here](doc/miscellaneous/comparison.md).
Here is a short example to illustrate interactive components: Here is a short example to illustrate interactive components:
```javascript ```javascript
const { Component, useState } = owl; const { Component, useState, mount } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
class Counter extends Component { class Counter extends Component {
@@ -63,8 +63,7 @@ class App extends Component {
static components = { Counter }; static components = { Counter };
} }
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
``` ```
Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate). Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
@@ -121,8 +120,7 @@ npm install @odoo/owl
If you want to use a simple `<script>` tag, the last release can be downloaded here: If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.0.6.js](https://github.com/odoo/owl/releases/download/v1.0.6/owl.js) - [owl-1.0.13](https://github.com/odoo/owl/releases/tag/v1.0.13)
- [owl-1.0.6.min.js](https://github.com/odoo/owl/releases/download/v1.0.6/owl.min.js)
## License ## License
+1 -2
View File
@@ -85,8 +85,7 @@ afterEach(() => {
describe("SomeComponent", () => { describe("SomeComponent", () => {
test("component behaves as expected", async () => { test("component behaves as expected", async () => {
const props = {...}; // depends on the component const props = {...}; // depends on the component
const comp = new SomeComponent(null, props); const comp = await mount(SomeComponent, { target: fixture, props });
await comp.mount(fixture);
// do some assertions // do some assertions
expect(...).toBe(...); expect(...).toBe(...);
+2 -2
View File
@@ -96,7 +96,7 @@ class OrderLine extends Component {
</div>`; </div>`;
add() { add() {
this.trigger("add-to-order", { line: props.line }); this.trigger("add-to-order", { line: this.props.line });
} }
} }
@@ -111,7 +111,7 @@ class Parent extends Component {
static components = { OrderLine }; static components = { OrderLine };
orders = useState([ orders = useState([
{ id: 1, name: "Coffee", quantity: 0 }, { id: 1, name: "Coffee", quantity: 0 },
{ id: 2, name: "Tea", quantity: 0 } { id: 2, name: "Tea", quantity: 0 },
]); ]);
addToOrder(event) { addToOrder(event) {
+19 -22
View File
@@ -54,7 +54,7 @@ Now, `index.html` should contain the following:
And `app.js` should look like this: And `app.js` should look like this:
```js ```js
const { Component } = owl; const { Component, mount } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
const { whenReady } = owl.utils; const { whenReady } = owl.utils;
@@ -65,8 +65,7 @@ class App extends Component {
// Setup code // Setup code
function setup() { function setup() {
const app = new App(); mount(App, target: { document.body })
app.mount(document.body);
} }
whenReady(setup); whenReady(setup);
@@ -124,7 +123,7 @@ Here is the content of `app.js` and `main.js`:
```js ```js
// app.js ---------------------------------------------------------------------- // app.js ----------------------------------------------------------------------
const { Component } = owl; const { Component, mount } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
export class App extends Component { export class App extends Component {
@@ -135,8 +134,7 @@ export class App extends Component {
import { App } from "./app.js"; import { App } from "./app.js";
function setup() { function setup() {
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
} }
owl.utils.whenReady(setup); owl.utils.whenReady(setup);
@@ -240,12 +238,11 @@ export class App extends Component {
} }
// src/main.js ----------------------------------------------------------------- // src/main.js -----------------------------------------------------------------
import { utils } from "@odoo/owl"; import { utils, mount } from "@odoo/owl";
import { App } from "./components/App"; import { App } from "./components/App";
function setup() { function setup() {
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
} }
utils.whenReady(setup); utils.whenReady(setup);
@@ -253,6 +250,7 @@ utils.whenReady(setup);
// tests/components/App.test.js ------------------------------------------------ // tests/components/App.test.js ------------------------------------------------
import { App } from "../../src/components/App"; import { App } from "../../src/components/App";
import { makeTestFixture, nextTick, click } from "../helpers"; import { makeTestFixture, nextTick, click } from "../helpers";
import { mount } from "@odoo/owl";
let fixture; let fixture;
@@ -266,8 +264,7 @@ afterEach(() => {
describe("App", () => { describe("App", () => {
test("Works as expected...", async () => { test("Works as expected...", async () => {
const app = new App(); await mount(App, { target: fixture });
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>"); expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
click(fixture, "div"); click(fixture, "div");
@@ -281,7 +278,7 @@ import { Component } from "@odoo/owl";
import "regenerator-runtime/runtime"; import "regenerator-runtime/runtime";
export async function nextTick() { export async function nextTick() {
return new Promise(function(resolve) { return new Promise(function (resolve) {
setTimeout(() => Component.scheduler.requestAnimationFrame(() => resolve())); setTimeout(() => Component.scheduler.requestAnimationFrame(() => resolve()));
}); });
} }
@@ -361,26 +358,26 @@ const HtmlWebpackPlugin = require("html-webpack-plugin");
const host = process.env.HOST || "localhost"; const host = process.env.HOST || "localhost";
module.exports = function(env, argv) { module.exports = function (env, argv) {
const mode = argv.mode || "development"; const mode = argv.mode || "development";
return { return {
mode: mode, mode: mode,
entry: "./src/main.js", entry: "./src/main.js",
output: { output: {
filename: "main.js", filename: "main.js",
path: path.resolve(__dirname, "dist") path: path.resolve(__dirname, "dist"),
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.jsx?$/, test: /\.jsx?$/,
loader: "babel-loader", loader: "babel-loader",
exclude: /node_modules/ exclude: /node_modules/,
} },
] ],
}, },
resolve: { resolve: {
extensions: [".js", ".jsx"] extensions: [".js", ".jsx"],
}, },
devServer: { devServer: {
contentBase: path.resolve(__dirname, "public/index.html"), contentBase: path.resolve(__dirname, "public/index.html"),
@@ -388,14 +385,14 @@ module.exports = function(env, argv) {
hot: true, hot: true,
host, host,
port: 3000, port: 3000,
publicPath: "/" publicPath: "/",
}, },
plugins: [ plugins: [
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
inject: true, inject: true,
template: path.resolve(__dirname, "public/index.html") template: path.resolve(__dirname, "public/index.html"),
}) }),
] ],
}; };
}; };
``` ```
+27 -32
View File
@@ -62,7 +62,7 @@ our application. `app.js` is where we will write all our code. For now, let's
just put the following code: just put the following code:
```js ```js
(function() { (function () {
console.log("hello owl", owl.__info__.version); console.log("hello owl", owl.__info__.version);
})(); })();
``` ```
@@ -83,7 +83,7 @@ a single root component. Let us start by defining an `App` component. Replace th
content of the function in `app.js` by the following code: content of the function in `app.js` by the following code:
```js ```js
const { Component } = owl; const { Component, mount } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
const { whenReady } = owl.utils; const { whenReady } = owl.utils;
@@ -94,8 +94,7 @@ class App extends Component {
// Setup code // Setup code
function setup() { function setup() {
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
} }
whenReady(setup); whenReady(setup);
@@ -170,13 +169,13 @@ class App extends Component {
{ {
id: 1, id: 1,
title: "buy milk", title: "buy milk",
isCompleted: true isCompleted: true,
}, },
{ {
id: 2, id: 2,
title: "clean house", title: "clean house",
isCompleted: false isCompleted: false,
} },
]; ];
} }
``` ```
@@ -279,8 +278,7 @@ class App extends Component {
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
function setup() { function setup() {
owl.config.mode = "dev"; owl.config.mode = "dev";
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
} }
whenReady(setup); whenReady(setup);
@@ -562,23 +560,23 @@ const actions = {
const task = { const task = {
id: state.nextId++, id: state.nextId++,
title: title, title: title,
isCompleted: false isCompleted: false,
}; };
state.tasks.push(task); state.tasks.push(task);
} }
}, },
toggleTask({ state }, id) { toggleTask({ state }, id) {
const task = state.tasks.find(t => t.id === id); const task = state.tasks.find((t) => t.id === id);
task.isCompleted = !task.isCompleted; task.isCompleted = !task.isCompleted;
}, },
deleteTask({ state }, id) { deleteTask({ state }, id) {
const index = state.tasks.findIndex(t => t.id === id); const index = state.tasks.findIndex((t) => t.id === id);
state.tasks.splice(index, 1); state.tasks.splice(index, 1);
} },
}; };
const initialState = { const initialState = {
nextId: 1, nextId: 1,
tasks: [] tasks: [],
}; };
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -616,7 +614,7 @@ class App extends Component {
static components = { Task }; static components = { Task };
inputRef = useRef("add-input"); inputRef = useRef("add-input");
tasks = useStore(state => state.tasks); tasks = useStore((state) => state.tasks);
dispatch = useDispatch(); dispatch = useDispatch();
mounted() { mounted() {
@@ -639,8 +637,7 @@ function setup() {
owl.config.mode = "dev"; owl.config.mode = "dev";
const store = new Store({ actions, state: initialState }); const store = new Store({ actions, state: initialState });
App.env.store = store; App.env.store = store;
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
} }
whenReady(setup); whenReady(setup);
@@ -666,9 +663,8 @@ function makeStore() {
function setup() { function setup() {
owl.config.mode = "dev"; owl.config.mode = "dev";
App.env.store = makeStore(); const env = {store = makeStore()};
const app = new App(); mount(App, { target: document.body, env });
app.mount(document.body);
} }
``` ```
@@ -811,7 +807,7 @@ For reference, here is the final code:
``` ```
```js ```js
(function() { (function () {
const { Component, Store } = owl; const { Component, Store } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
const { whenReady } = owl.utils; const { whenReady } = owl.utils;
@@ -827,24 +823,24 @@ For reference, here is the final code:
const task = { const task = {
id: state.nextId++, id: state.nextId++,
title: title, title: title,
isCompleted: false isCompleted: false,
}; };
state.tasks.push(task); state.tasks.push(task);
} }
}, },
toggleTask({ state }, id) { toggleTask({ state }, id) {
const task = state.tasks.find(t => t.id === id); const task = state.tasks.find((t) => t.id === id);
task.isCompleted = !task.isCompleted; task.isCompleted = !task.isCompleted;
}, },
deleteTask({ state }, id) { deleteTask({ state }, id) {
const index = state.tasks.findIndex(t => t.id === id); const index = state.tasks.findIndex((t) => t.id === id);
state.tasks.splice(index, 1); state.tasks.splice(index, 1);
} },
}; };
const initialState = { const initialState = {
nextId: 1, nextId: 1,
tasks: [] tasks: [],
}; };
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -897,7 +893,7 @@ For reference, here is the final code:
static components = { Task }; static components = { Task };
inputRef = useRef("add-input"); inputRef = useRef("add-input");
tasks = useStore(state => state.tasks); tasks = useStore((state) => state.tasks);
filter = useState({ value: "all" }); filter = useState({ value: "all" });
dispatch = useDispatch(); dispatch = useDispatch();
@@ -916,9 +912,9 @@ For reference, here is the final code:
get displayedTasks() { get displayedTasks() {
switch (this.filter.value) { switch (this.filter.value) {
case "active": case "active":
return this.tasks.filter(t => !t.isCompleted); return this.tasks.filter((t) => !t.isCompleted);
case "completed": case "completed":
return this.tasks.filter(t => t.isCompleted); return this.tasks.filter((t) => t.isCompleted);
case "all": case "all":
return this.tasks; return this.tasks;
} }
@@ -943,9 +939,8 @@ For reference, here is the final code:
function setup() { function setup() {
owl.config.mode = "dev"; owl.config.mode = "dev";
App.env.store = makeStore(); const env = {store = makeStore()};
const app = new App(); mount(App, { target: document.body, env });
app.mount(document.body);
} }
whenReady(setup); whenReady(setup);
+3 -3
View File
@@ -257,11 +257,11 @@ to the store like in redux, with the `useStore` hook (see the [store documentati
const actions = { const actions = {
increment({ state }, val) { increment({ state }, val) {
state.counter.value += val; state.counter.value += val;
} },
}; };
const state = { const state = {
counter: { value: 0 } counter: { value: 0 },
}; };
const store = new owl.Store({ state, actions }); const store = new owl.Store({ state, actions });
@@ -270,7 +270,7 @@ class Counter extends Component {
<button t-name="Counter" t-on-click="dispatch('increment')"> <button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="counter.value"/>] Click Me! [<t t-esc="counter.value"/>]
</button>`; </button>`;
counter = useStore(state => state.counter); counter = useStore((state) => state.counter);
dispatch = useDispatch(); dispatch = useDispatch();
} }
+12 -12
View File
@@ -61,14 +61,14 @@ because a lot of the state is hidden in their internals.
React or Vue have a huge community, and a lot of effort have been made into their React or Vue have a huge community, and a lot of effort have been made into their
tooling. This is wonderful, but at the same time, a pretty big issue for Odoo: tooling. This is wonderful, but at the same time, a pretty big issue for Odoo:
since the assets are totally dynamic (and could change whenever the user install since the assets are totally dynamic (and could change whenever the user installs
or remove an addon), we need to have all that kind of tooling on the production or removes an addon), we need to have all that kind of tooling on the production
servers. This is certainly not ideal. servers. This is certainly not ideal.
Also, this makes it very complicated to setup Vue or React tools: Odoo code is Also, this makes it very complicated to setup Vue or React tools: Odoo code is
not a simple file that import other files. It changes all the time, assets not a simple file that import other files. It changes all the time, assets
are bundled differently in different contexts. This is the reason why Odoo has are bundled differently in different contexts. This is the reason why Odoo has
its own module system, which are resolve at runtime, by the browser. The its own module system, which are resolved at runtime, by the browser. The
dynamic nature of Odoo means that we often need to delay work as late as possible dynamic nature of Odoo means that we often need to delay work as late as possible
(in other word, we want a JIT user interface!) (in other word, we want a JIT user interface!)
@@ -78,12 +78,12 @@ deploy. Using React without JSX, or Vue without vue file is not very appealing.
At the same time, Owl is designed to solve this issue: it compiles templates At the same time, Owl is designed to solve this issue: it compiles templates
by the browser, it doesn't need much code for that, since we use the XML parser by the browser, it doesn't need much code for that, since we use the XML parser
built into each browser. Owl works with or without any additional tooling. It built into each browser. Owl works with or without any additional tooling. It
can use template strings to write single file component, and is easy to integrate can use template strings to write single file components, and is easy to integrate
in any html page, with a simple `<script>` tag. in any html page, with a simple `<script>` tag.
## Template based ## Template based
Odoo stores template as XML document in a database. This is very powerful, since Odoo stores templates as XML documents in a database. This is very powerful, since
this allow the use of xpaths to customize other templates. This is a very this allow the use of xpaths to customize other templates. This is a very
important feature of odoo, and one of the key to Odoo modularity. important feature of odoo, and one of the key to Odoo modularity.
@@ -104,12 +104,12 @@ awkward, and very confusing.
## Developer Experience ## Developer Experience
This brings us to the following point: developer experience. We see this choice This brings us to the following point: developer experience. We see this choice
as an investment for the future, and we want to make onboarding developer as as an investment for the future, and we want to make onboarding developers as
easy as possible. easy as possible.
While many javascript professionals clearly think that react/vue is not difficult While many javascript professionals clearly think that react/vue is not difficult
(which is true to some extent), it is alsy true that many non js specialists are (which is true to some extent), it is alsy true that many non js specialists are
overwhelmed with the frontend world: functional component, hooks, and many other overwhelmed with the frontend world: functional components, hooks, and many other
fancy words. Also, what is available in the compilation context may be difficult, fancy words. Also, what is available in the compilation context may be difficult,
there is a lot of black magic going on in pretty much every framework. Vue there is a lot of black magic going on in pretty much every framework. Vue
somehow join various namespaces into one, under the hood, and add various internal somehow join various namespaces into one, under the hood, and add various internal
@@ -135,7 +135,7 @@ needs: Odoo will fetch templates from the database and need to compile them only
at the last possible moment, so we can apply all necessary xpaths. at the last possible moment, so we can apply all necessary xpaths.
Even more: Odoo needs to be able to generate (and compile) templates at runtime. Even more: Odoo needs to be able to generate (and compile) templates at runtime.
Currently, Odoo form views interpret a xml description. But the form view code Currently, Odoo form views interpret an xml description. But the form view code
then needs to do a lot of complicated operations. With Owl, we will be able to then needs to do a lot of complicated operations. With Owl, we will be able to
transform a view description into a QWeb template, then compile that and use it transform a view description into a QWeb template, then compile that and use it
immediately. immediately.
@@ -147,16 +147,16 @@ For example, the reactivity system. We like the way Vue did it, but it has a
flaw: it is not really optional. There is actually a way to opt out of the reactivity flaw: it is not really optional. There is actually a way to opt out of the reactivity
system by freezing the state, but then, it is freezed. system by freezing the state, but then, it is freezed.
And there certainly are situations where we need a state, which is not readonly, And there certainly are situations where we need a state, which is not read-only,
and not observed. For example, imagine a spreadsheet component. It may have a and not observed. For example, imagine a spreadsheet component. It may have a
very large internal state, and it knows exactly when it needs to be rendered very large internal state, and it knows exactly when it needs to be rendered
(basically, whenever the user perform some action). Then, observing its state (basically, whenever the user performs some action). Then, observing its state
is a net performance loss, both for the CPU and the memory. is a net performance loss, both for the CPU and the memory.
## Concurrency ## Concurrency
Many applications are happy to simply display a spinner whenever a new asynchronous Many applications are happy to simply display a spinner whenever a new asynchronous
action is performed, but Odoo want a different user experience: most asynchronous action is performed, but Odoo wants a different user experience: most asynchronous
state changes are not displayed until ready. This is sometimes called a concurrent state changes are not displayed until ready. This is sometimes called a concurrent
mode: the UI is rendered in memory, and displayed only when it is ready (and mode: the UI is rendered in memory, and displayed only when it is ready (and
only if it has not been cancelled by subsequent user actions). only if it has not been cancelled by subsequent user actions).
@@ -175,6 +175,6 @@ that current standard frameworks are not tailored to our needs. It is perfectly
fine, because they each chose a different set of tradeoffs. fine, because they each chose a different set of tradeoffs.
However, we feel that there is still room in the framework world for something However, we feel that there is still room in the framework world for something
that is different. For a framework that make choices compatible with Odoo. that is different. For a framework that makes choices compatible with Odoo.
And that is why we built Owl 🦉. And that is why we built Owl 🦉.
+1
View File
@@ -27,6 +27,7 @@ provided by Owl.
- [Event Handling](reference/event_handling.md) - [Event Handling](reference/event_handling.md)
- [Error Handling](reference/error_handling.md) - [Error Handling](reference/error_handling.md)
- [Hooks](reference/hooks.md) - [Hooks](reference/hooks.md)
- [Mounting a component](reference/mounting.md)
- [Miscellaneous Components](reference/misc.md) - [Miscellaneous Components](reference/misc.md)
- [Observer](reference/observer.md) - [Observer](reference/observer.md)
- [Props](reference/props.md) - [Props](reference/props.md)
+8 -4
View File
@@ -204,7 +204,7 @@ to be called in the constructor.
class Counter extends owl.Component { class Counter extends owl.Component {
static props = { static props = {
initialValue: Number, initialValue: Number,
optional: true optional: true,
}; };
} }
``` ```
@@ -217,7 +217,7 @@ to be called in the constructor.
```js ```js
class Counter extends owl.Component { class Counter extends owl.Component {
static defaultProps = { static defaultProps = {
initialValue: 0 initialValue: 0,
}; };
} }
``` ```
@@ -270,6 +270,10 @@ We explain here all the public methods of the `Component` class.
// app is now visible // app is now visible
``` ```
Note that the normal way of mounting an application is by using the `mount`
method on a component class, not by creating the instance by hand. See the
documentation on [mounting applications](mounting.md).
* **`unmount()`**: in case a component needs to be detached/removed from the DOM, this * **`unmount()`**: in case a component needs to be detached/removed from the DOM, this
method can be used. Most applications should not call `unmount`, this is more method can be used. Most applications should not call `unmount`, this is more
useful to the underlying component system. useful to the underlying component system.
@@ -804,8 +808,8 @@ class RootNode extends Component {
children: [ children: [
{ label: "b" }, { label: "b" },
{ label: "c", children: [{ label: "d" }, { label: "e" }] }, { label: "c", children: [{ label: "d" }, { label: "e" }] },
{ label: "f", children: [{ label: "g" }] } { label: "f", children: [{ label: "g" }] },
] ],
}; };
} }
``` ```
+19 -2
View File
@@ -2,9 +2,10 @@
The Owl framework is designed to work in many situations. However, it is The Owl framework is designed to work in many situations. However, it is
sometimes necessary to customize some behaviour. This is done by using the sometimes necessary to customize some behaviour. This is done by using the
global `config` object. It currently has one key: global `config` object. It provides two settings:
- [`mode`](#mode). - [`mode`](#mode) (default value: `prod`),
- [`enableTransitions`](#enabletransitions) (default value: `true`).
## Mode ## Mode
@@ -25,3 +26,19 @@ So, changing this setting is best done at startup.
An important job done by the `dev` mode is to validate props for each component An important job done by the `dev` mode is to validate props for each component
creation and update. Also, extra props will cause an error. creation and update. Also, extra props will cause an error.
## `enableTransitions`
Transitions are usually nice, but they can cause issues in some specific cases,
such as automated tests. It is uncomfortable having to wait for a transition
to end before moving to the next step.
To solve this issue, Owl can be configured to ignore the `t-transition` directive.
To do that, one only needs to set the `enableTransitions` flag to false:
```js
owl.config.enableTransitions = false;
```
Note that it suffers from the same drawback as the "dev" mode: all compiled
templates, if any, will keep their current behaviours.
+5 -4
View File
@@ -10,10 +10,11 @@ exported as `owl.core.EventBus`.
Component misc Component misc
Context AsyncRoot Context AsyncRoot
QWeb Portal QWeb Portal
Store router mount router
useState Link Store Link
config RouteComponent useState RouteComponent
mode Router config Router
mode
core tags core tags
EventBus css EventBus css
Observer xml Observer xml
+6 -8
View File
@@ -49,15 +49,14 @@ The correct way to customize an environment is to simply set it up on the root
component class, before the first component is created: component class, before the first component is created:
```js ```js
App.env = { const env = {
_t: myTranslateFunction, _t: myTranslateFunction,
user: {...}, user: {...},
services: { services: {
... ...
}, },
}; };
const app = new App(); mount(App, { target: document.body, env });
app.mount(document.body);
``` ```
It is also possible to simply share an environment between all root components, It is also possible to simply share an environment between all root components,
@@ -113,17 +112,16 @@ async function myEnv() {
qweb: qweb, qweb: qweb,
services: { services: {
localStorage: localStorage, localStorage: localStorage,
rpc: rpc rpc: rpc,
}, },
debug: false, debug: false,
inMobileMode: true inMobileMode: true,
}; };
} }
async function start() { async function start() {
App.env = await myEnv(); const env = await myEnv();
const app = new App(); mount(App, { target: document.body, env });
await app.mount(document.body);
} }
``` ```
+1 -1
View File
@@ -74,7 +74,7 @@ broadcasted to the application by an event on the `qweb` instance. It may be
useful, for example, to log the error somewhere. useful, for example, to log the error somewhere.
```js ```js
env.qweb.on("error", null, function(error) { env.qweb.on("error", null, function (error) {
// do something // do something
// react to the error // react to the error
}); });
+1 -1
View File
@@ -7,7 +7,7 @@ triggering events, and callbacks.
```js ```js
const bus = new owl.core.EventBus(); const bus = new owl.core.EventBus();
bus.on("some-event", null, function(...args) { bus.on("some-event", null, function (...args) {
console.log(...args); console.log(...args);
}); });
+14
View File
@@ -78,6 +78,20 @@ The `t-on` directive allows to prebind its arguments. For example,
Here, `expr` is a valid Owl expression, so it could be `true` or some variable Here, `expr` is a valid Owl expression, so it could be `true` or some variable
from the rendering context. from the rendering context.
### Type Hinting
Note that if you work with Typescript, the `trigger` method is generic on the type of the payload.
You can then describe the type of the event, so you will see typing errors...
```typescript
this.trigger<MyCustomPayload>("my-custom-event", payload);
```
```typescript
myCustomEventHandler(ev: OwlEvent<MyCustomPayload>) { ... }
```
## Inline Event Handlers ## Inline Event Handlers
One can also directly specify inline statements. For example, One can also directly specify inline statements. For example,
+1 -1
View File
@@ -259,7 +259,7 @@ function useLoader() {
} }
onWillStart(() => updateRecord(component.props.id)); onWillStart(() => updateRecord(component.props.id));
onWillUpdateProps(nextProps => updateRecord(nextProps.id)); onWillUpdateProps((nextProps) => updateRecord(nextProps.id));
return record; return record;
} }
+2 -3
View File
@@ -43,7 +43,7 @@ workflow to help the user put in some data, which it could use later on.
JavaScript: JavaScript:
```js ```js
const { Component } = owl; const { Component, mount } = owl;
const { Portal } = owl.misc; const { Portal } = owl.misc;
class TeleportedComponent extends Component {} class TeleportedComponent extends Component {}
@@ -51,8 +51,7 @@ class App extends Component {
static components = { Portal, TeleportedComponent }; static components = { Portal, TeleportedComponent };
} }
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
``` ```
XML: XML:
+60
View File
@@ -0,0 +1,60 @@
# 🦉 Mounting an application 🦉
## Content
- [Overview](#overview)
- [API](#api)
## Overview
Mounting an Owl application is done by using the `mount` method (available in
`owl.mount` if you are using the iife build, or it can be directly imported
from `owl` if you are using a module system):
```js
const mount = { owl }; // if owl is available as an object
const env = { ... };
const app = await mount(MyComponent, { target: document.body, env });
```
Another example:
```js
const config = {
env: ...,
props: ...,
target: document.body,
position: "self",
};
const app = await mount(App, config);
```
A common way to initialize an application is to first setup an environment,
then to call the `mount` method.
## API
Mount takes two parameters:
- `C`, which should be a component class (NOT instance),
- `params`, which is an object with the following keys:
- `target (HTMLElement | DocumentFragment)`: the target of the mount operation
- `env (optional, Env)` an environment
- `position (optional, "first-child" | "last-child" | "self")` the position
where it should be mounted (see below for more informations)
- `props (optional, any)`: some initial values that are given as props. Useful
when the root component is configurable, or when testing sub components
Here are the various positions supported by Owl:
- `first-child`: with this option, the component will be prepended inside the target,
- `last-child` (default value): with this option, the component will be
appended in the target element,
- `self`: the target will be used as the root element for the component. This
means that the target has to be an HTMLElement (and not a document fragment).
In this situation, it is possible that the component cannot be unmounted. For
example, if its target is `document.body`.
The `mount` method returns a promise that resolves to the instance of the created
component.
+1 -1
View File
@@ -18,7 +18,7 @@ class Child extends Component {
} }
class Parent extends Component { class Parent extends Component {
static template = xml`<div><ComponentA a="state.a" b="'string'"/></div>`; static template = xml`<div><Child a="state.a" b="'string'"/></div>`;
static components = { Child }; static components = { Child };
state = useState({ a: "fromparent" }); state = useState({ a: "fromparent" });
} }
+1 -1
View File
@@ -30,7 +30,7 @@ class ComponentB extends owl.Component {
count: {type: Number}, count: {type: Number},
messages: { messages: {
type: Array, type: Array,
element: {type: Object, shape: {id: Boolean, text: 'string' } element: {type: Object, shape: {id: Boolean, text: String }
}, },
date: Date, date: Date,
combinedVal: [Number, Boolean] combinedVal: [Number, Boolean]
+2 -2
View File
@@ -114,9 +114,9 @@ For example:
const translations = { const translations = {
hello: "bonjour", hello: "bonjour",
yes: "oui", yes: "oui",
no: "non" no: "non",
}; };
const translateFn = str => translations[str] || str; const translateFn = (str) => translations[str] || str;
const qweb = new QWeb({ translateFn }); const qweb = new QWeb({ translateFn });
``` ```
+28
View File
@@ -15,6 +15,7 @@
- [Dynamic Attributes](#dynamic-attributes) - [Dynamic Attributes](#dynamic-attributes)
- [Loops](#loops) - [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates) - [Rendering Sub Templates](#rendering-sub-templates)
- [Dynamic Sub Templates](#dynamic-sub-templates)
- [Translations](#translations) - [Translations](#translations)
- [Debugging](#debugging) - [Debugging](#debugging)
@@ -452,6 +453,17 @@ are all equivalent:
If there is no `t-key` directive, Owl will use the index as a default key. If there is no `t-key` directive, Owl will use the index as a default key.
Note: the `t-foreach` directive only accepts arrays (lists) or objects. It does
not work with other iterables, such as `Set`. However, it is only a matter of
using the `...` javascript operator. For example:
```xml
<t t-foreach="...items" t-as="item">...</t>
```
The `...` operator will convert the `Set` (or any other iterables) into a list,
which will work with Owl QWeb.
### Rendering Sub Templates ### Rendering Sub Templates
QWeb templates can be used for top level rendering, but they can also be used QWeb templates can be used for top level rendering, but they can also be used
@@ -509,6 +521,22 @@ This can be used to define variables scoped to a sub template:
<!-- "var" does not exist here --> <!-- "var" does not exist here -->
``` ```
### Dynamic sub templates
The `t-call` directive can also be used to dynamically call a sub template,
using string interpolation. For example:
```xml
<div t-name="main-template">
<t t-call="{{template}}">
<em>content</em>
</t>
</div>
```
Here, the name of the template is obtained from the `template` value in the
template rendering context.
### Translations ### Translations
By default, QWeb specify that templates should be translated. If this behaviour By default, QWeb specify that templates should be translated. If this behaviour
+17 -2
View File
@@ -25,6 +25,8 @@ some sub template, but still be the owner. For example, a generic dialog compone
will need to render some content, some footer, but with the parent as the will need to render some content, some footer, but with the parent as the
rendering context. rendering context.
Slots are inserted with the `t-slot` directive:
```xml ```xml
<div t-name="Dialog" class="modal"> <div t-name="Dialog" class="modal">
<div class="modal-title"><t t-esc="props.title"/></div> <div class="modal-title"><t t-esc="props.title"/></div>
@@ -62,7 +64,9 @@ This is deprecated and should no longer be used in new code.
## Reference ## Reference
Default slot: the first element inside the component which is not a named slot will ### Default Slot
The first element inside the component which is not a named slot will
be considered the `default` slot. For example: be considered the `default` slot. For example:
```xml ```xml
@@ -77,7 +81,9 @@ be considered the `default` slot. For example:
</div> </div>
``` ```
Default content: slots can define a default content, in case the parent did not define them: ### Default content
Slots can define a default content, in case the parent did not define them:
```xml ```xml
<div t-name="Parent"> <div t-name="Parent">
@@ -94,3 +100,12 @@ Rendering context: the content of the slots is actually rendered with the
rendering context corresponding to where it was defined, not where it is rendering context corresponding to where it was defined, not where it is
positioned. This allows the user to define event handlers that will be bound positioned. This allows the user to define event handlers that will be bound
to the correct component (usually, the grandparent of the slot content). to the correct component (usually, the grandparent of the slot content).
### Dynamic Slots
The `t-slot` directive is actually able to use any expressions, using string
interplolation:
```xml
<t t-slot="{{current}}" />
```
+14 -14
View File
@@ -40,14 +40,14 @@ const actions = {
state.todos.push({ state.todos.push({
id: state.nextId++, id: state.nextId++,
message, message,
isCompleted: false isCompleted: false,
}); });
} },
}; };
const state = { const state = {
todos: [], todos: [],
nextId: 1 nextId: 1,
}; };
const store = new owl.Store({ state, actions }); const store = new owl.Store({ state, actions });
@@ -90,7 +90,7 @@ const config = {
state, state,
actions, actions,
getters, getters,
env env,
}; };
const store = new Store(config); const store = new Store(config);
``` ```
@@ -110,7 +110,7 @@ const actions = {
} catch (e) { } catch (e) {
state.loginState = "error"; state.loginState = "error";
} }
} },
}; };
``` ```
@@ -144,7 +144,7 @@ const actions = {
state.recordId = recordId; state.recordId = recordId;
const data = await doSomeRPC("/read/", recordId); const data = await doSomeRPC("/read/", recordId);
state.recordData = data; state.recordData = data;
} },
}; };
``` ```
@@ -159,7 +159,7 @@ const actions = {
const data = await doSomeRPC("/read/", recordId); const data = await doSomeRPC("/read/", recordId);
state.recordId = recordId; state.recordId = recordId;
state.recordData = data; state.recordData = data;
} },
}; };
``` ```
@@ -187,14 +187,14 @@ transform the data contained in the store.
```js ```js
const getters = { const getters = {
getPost({ state }, id) { getPost({ state }, id) {
const post = state.posts.find(p => p.id === id); const post = state.posts.find((p) => p.id === id);
const author = state.authors.find(a => a.id === post.id); const author = state.authors.find((a) => a.id === post.id);
return { return {
id, id,
author, author,
content: post.content content: post.content,
}; };
} },
}; };
// somewhere else // somewhere else
@@ -224,11 +224,11 @@ Assume we have this store:
const actions = { const actions = {
increment({ state }, val) { increment({ state }, val) {
state.counter.value += val; state.counter.value += val;
} },
}; };
const state = { const state = {
counter: { value: 0 } counter: { value: 0 },
}; };
const store = new owl.Store({ state, actions }); const store = new owl.Store({ state, actions });
``` ```
@@ -245,7 +245,7 @@ A counter component can then select this value and dispatch an action like this:
```js ```js
class Counter extends Component { class Counter extends Component {
counter = useStore(state => state.counter); counter = useStore((state) => state.counter);
dispatch = useDispatch(); dispatch = useDispatch();
} }
+6 -6
View File
@@ -19,20 +19,20 @@ not ready yet, resolved directly otherwise). If called with a callback as
argument, it executes it as soon as the DOM ready (or directly). argument, it executes it as soon as the DOM ready (or directly).
```js ```js
Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function([templates]) { Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function ([templates]) {
const qweb = new owl.QWeb({ templates }); const qweb = new owl.QWeb({ templates });
const app = new App({ qweb }); const env = { qweb };
app.mount(document.body); await mount(App, { env, target: document.body });
}); });
``` ```
or alternatively: or alternatively:
```js ```js
owl.utils.whenReady(function() { owl.utils.whenReady(function () {
const qweb = new owl.QWeb(); const qweb = new owl.QWeb();
const app = new App({ qweb }); const env = { qweb };
app.mount(document.body); await mount(App, { env, target: document.body });
}); });
``` ```
+15 -15
View File
@@ -1,22 +1,20 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "1.0.6", "version": "1.0.13",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl.js", "main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js",
"module": "dist/owl.es.js",
"types": "dist/types/index.d.ts", "types": "dist/types/index.d.ts",
"files": [ "files": [
"dist/types/", "dist"
"dist/owl.js"
], ],
"engines": { "engines": {
"node": ">=10.15.3" "node": ">=10.15.3"
}, },
"scripts": { "scripts": {
"build:js": "tsc --target esnext --module es6 --outDir dist/owl", "dev": "rollup -c",
"build:bundle": "rollup -c", "build": "NODE_ENV=production rollup -c",
"build": "npm run build:js && npm run build:bundle",
"buildcommonjs": "npm run build:js && npm run build:bundle -- -f cjs",
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"tools:serve": "python3 tools/server.py || python tools/server.py", "tools:serve": "python3 tools/server.py || python tools/server.py",
@@ -24,7 +22,7 @@
"pretools:watch": "npm run build", "pretools:watch": "npm run build",
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"", "tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write", "prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
"publish": "npm run buildcommonjs && npm publish", "publish": "npm run build && npm publish",
"release": "node tools/release.js" "release": "node tools/release.js"
}, },
"repository": { "repository": {
@@ -37,9 +35,9 @@
"url": "https://github.com/odoo/owl/issues" "url": "https://github.com/odoo/owl/issues"
}, },
"homepage": "https://github.com/odoo/owl#readme", "homepage": "https://github.com/odoo/owl#readme",
"dependencies": {},
"devDependencies": { "devDependencies": {
"@types/jest": "^23.3.12", "@types/jest": "^23.3.14",
"@types/node": "^14.11.8",
"chalk": "^3.0.0", "chalk": "^3.0.0",
"cpx": "^1.5.0", "cpx": "^1.5.0",
"git-rev-sync": "^1.12.0", "git-rev-sync": "^1.12.0",
@@ -48,9 +46,10 @@
"jest-environment-jsdom": "^24.7.1", "jest-environment-jsdom": "^24.7.1",
"live-server": "^1.2.1", "live-server": "^1.2.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^1.19.1", "prettier": "^2.0.4",
"rollup": "^1.6.0", "rollup": "^1.6.0",
"rollup-plugin-typescript2": "^0.20.1", "rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.27.3",
"sass": "^1.16.1", "sass": "^1.16.1",
"source-map-support": "^0.5.10", "source-map-support": "^0.5.10",
"ts-jest": "^23.10.5", "ts-jest": "^23.10.5",
@@ -77,6 +76,7 @@
] ]
}, },
"prettier": { "prettier": {
"printWidth": 100 "printWidth": 100,
"endOfLine": "auto"
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉 # 🦉 OWL Roadmap 🦉
- Current version: 1.0.6 - Current version: 1.0.13
- Status: stable - Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may This roadmap is only an attempt at predicting Owl's future. Everything may
+66 -10
View File
@@ -1,14 +1,70 @@
import { version } from "./package.json"; import pkg from "./package.json";
import git from "git-rev-sync"; import git from "git-rev-sync";
import typescript from 'rollup-plugin-typescript2';
import { terser } from "rollup-plugin-terser";
const name = "owl";
const extend = true;
/**
* Meta data to be added on the __info__ object.
* Used to let external tools know the current owl version.
*/
const outro = `
__info__.version = '${pkg.version}';
__info__.date = '${new Date().toISOString()}';
__info__.hash = '${git.short()}';
__info__.url = 'https://github.com/odoo/owl';
`;
/**
* Generate from a string depicting a path a new path for the minified version.
* @param {string} pkgFileName file name
*/
function generateMinifiedNameFromPkgName(pkgFileName) {
const parts = pkgFileName.split('.');
parts.splice(parts.length - 1, 0, "min");
return parts.join('.');
}
/**
* Get the rollup config based on the arguments
* @param {string} format format of the bundle
* @param {string} generatedFileName generated file name
* @param {boolean} minified should it be minified
*/
function getConfigForFormat(format, generatedFileName, minified = false) {
return {
file: minified ? generateMinifiedNameFromPkgName(generatedFileName) : generatedFileName,
format: format,
name: name,
extend: extend,
outro: outro,
plugins: minified ? [terser()] : [],
indent: ' ', // indent with 4 spaces
};
}
// rollup.config.js
export default { export default {
input: "dist/owl/index.js", input: "src/index.ts",
output: { output: [
file: "dist/owl.js",
format: "iife", /**
name: "owl", * Read about module formats:
extend: true, * https://auth0.com/blog/javascript-module-systems-showdown/
outro: `exports.__info__.version = '${version}';\nexports.__info__.date = '${new Date().toISOString()}';\nexports.__info__.hash = '${git.short()}';\nexports.__info__.url = 'https://github.com/odoo/owl';` * https://medium.com/@kelin2025/so-you-wanna-use-es6-modules-714f48b3a953
} */
getConfigForFormat('esm', pkg.module),
getConfigForFormat('esm', pkg.module, true),
getConfigForFormat('cjs', pkg.main),
getConfigForFormat('cjs', pkg.main, true),
getConfigForFormat('iife', pkg.browser),
getConfigForFormat('iife', pkg.browser, true),
],
plugins: [
typescript({
useTsconfigDeclarationDir: true
}),
]
}; };
+1 -1
View File
@@ -19,5 +19,5 @@ export const browser: Browser = {
random: Math.random, random: Math.random,
Date: window.Date, Date: window.Date,
fetch: (window.fetch || (() => {})).bind(window), fetch: (window.fetch || (() => {})).bind(window),
localStorage: window.localStorage localStorage: window.localStorage,
}; };
+105 -15
View File
@@ -8,6 +8,7 @@ import "./props_validation";
import { Scheduler, scheduler } from "./scheduler"; import { Scheduler, scheduler } from "./scheduler";
import { activateSheet } from "./styles"; import { activateSheet } from "./styles";
import { Browser, browser } from "../browser"; import { Browser, browser } from "../browser";
import { onMounted, onPatched } from "../hooks";
/** /**
* Owl Component System * Owl Component System
@@ -91,8 +92,24 @@ interface Internal<T extends Env> {
refs: { [key: string]: Component<any, T> | HTMLElement | undefined } | null; refs: { [key: string]: Component<any, T> | HTMLElement | undefined } | null;
} }
interface DevToolsAccess {
props?: any;
defaultProps?: any;
template?: string | null;
state: Observer;
tag: String;
depth: number,
}
export const portalSymbol = Symbol("portal"); // FIXME export const portalSymbol = Symbol("portal"); // FIXME
/**
* It is required for the dev tools to have access to the __owl__ element.
*/
interface HTMLElementWithDevToolsAccess extends HTMLElement {
__owl_devtools__: DevToolsAccess
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Component // Component
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -110,11 +127,13 @@ export class Component<Props extends {} = any, T extends Env = Env> {
// expose scheduler s.t. it can be mocked for testing purposes // expose scheduler s.t. it can be mocked for testing purposes
static scheduler: Scheduler = scheduler; static scheduler: Scheduler = scheduler;
__devtools__: DevToolsAccess;
/** /**
* The `el` is the root element of the component. Note that it could be null: * The `el` is the root element of the component. Note that it could be null:
* this is the case if the component is not mounted yet, or is destroyed. * this is the case if the component is not mounted yet, or is destroyed.
*/ */
get el(): HTMLElement | null { get el(): HTMLElementWithDevToolsAccess | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null; return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
} }
@@ -203,11 +222,29 @@ export class Component<Props extends {} = any, T extends Env = Env> {
renderFn: qweb.render.bind(qweb, template), renderFn: qweb.render.bind(qweb, template),
classObj: null, classObj: null,
refs: null, refs: null,
scope: null scope: null,
}; };
if (constr.style) { if (constr.style) {
this.__applyStyles(constr); this.__applyStyles(constr);
} }
// DevTools hooks
onMounted(() => {
this.__devtools__ = {
depth: this.__owl__.depth,
state: this.__owl__.observer,
tag: this.constructor.name
};
this.__devtools__.defaultProps = defaultProps;
this.__devtools__.props = this.props;
this.__devtools__.template = template;
this.el.__owl_devtools__ = this.__devtools__;
})
onPatched(() => {
this.__devtools__.depth = this.__owl__.depth,
this.el.__owl_devtools__ = this.__devtools__;
})
} }
/** /**
@@ -316,6 +353,9 @@ export class Component<Props extends {} = any, T extends Env = Env> {
return Promise.resolve(); return Promise.resolve();
} }
} }
if (__owl__.isDestroyed) {
throw new Error("Cannot mount a destroyed component");
}
if (__owl__.currentFiber) { if (__owl__.currentFiber) {
const currentFiber = __owl__.currentFiber; const currentFiber = __owl__.currentFiber;
if (currentFiber.target === target && currentFiber.position === position) { if (currentFiber.target === target && currentFiber.position === position) {
@@ -430,8 +470,8 @@ export class Component<Props extends {} = any, T extends Env = Env> {
* up to the parent DOM nodes. Thus, it must be called between mounted() and * up to the parent DOM nodes. Thus, it must be called between mounted() and
* willUnmount(). * willUnmount().
*/ */
trigger(eventType: string, payload?: any) { trigger<T = any>(eventType: string, payload?: T) {
this.__trigger(this, eventType, payload); this.__trigger<T>(this, eventType, payload);
} }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -493,9 +533,9 @@ export class Component<Props extends {} = any, T extends Env = Env> {
} }
this.willUnmount(); this.willUnmount();
__owl__.isMounted = false; __owl__.isMounted = false;
if (this.__owl__.currentFiber) { if (__owl__.currentFiber) {
this.__owl__.currentFiber.isCompleted = true; __owl__.currentFiber.isCompleted = true;
this.__owl__.currentFiber.root.counter = 0; __owl__.currentFiber.root.counter = 0;
} }
const children = __owl__.children; const children = __owl__.children;
for (let id in children) { for (let id in children) {
@@ -509,12 +549,12 @@ export class Component<Props extends {} = any, T extends Env = Env> {
* Private trigger method, allows to choose the component which triggered * Private trigger method, allows to choose the component which triggered
* the event in the first place * the event in the first place
*/ */
__trigger(component: Component, eventType: string, payload?: any) { __trigger<T>(component: Component, eventType: string, payload?: T) {
if (this.el) { if (this.el) {
const ev = new OwlEvent(component, eventType, { const ev = new OwlEvent<T>(component, eventType, {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
detail: payload detail: payload,
}); });
const triggerHook = this.env[portalSymbol as any]; const triggerHook = this.env[portalSymbol as any];
if (triggerHook) { if (triggerHook) {
@@ -549,7 +589,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
} }
await Promise.all([ await Promise.all([
this.willUpdateProps(nextProps), this.willUpdateProps(nextProps),
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps) __owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps),
]); ]);
if (fiber.isCompleted) { if (fiber.isCompleted) {
return; return;
@@ -646,16 +686,35 @@ export class Component<Props extends {} = any, T extends Env = Env> {
try { try {
let vnode = __owl__.renderFn!(this, { let vnode = __owl__.renderFn!(this, {
handlers: __owl__.boundHandlers, handlers: __owl__.boundHandlers,
fiber: fiber fiber: fiber,
}); });
// we iterate over the children to detect those that no longer belong to the // we iterate over the children to detect those that no longer belong to the
// current rendering: those ones, if not mounted yet, can (and have to) be // current rendering: those ones, if not mounted yet, can (and have to) be
// destroyed right now, because they are not in the DOM, and thus we won't // destroyed right now, because they are not in the DOM, and thus we won't
// be notified later on (when patching), that they are removed from the DOM // be notified later on (when patching), that they are removed from the DOM
for (let childKey in __owl__.children) { for (let childKey in __owl__.children) {
let child = __owl__.children[childKey]; const child = __owl__.children[childKey];
if (!child.__owl__.isMounted && child.__owl__.parentLastFiberId < fiber.id) { const childOwl = child.__owl__;
child.destroy(); if (!childOwl.isMounted && childOwl.parentLastFiberId < fiber.id) {
// we only do here a "soft" destroy, meaning that we leave the child
// dom node alone, without removing it. Most of the time, it does not
// matter, because the child component is already unmounted. However,
// if some of its parent have been unmounted, the child could actually
// still be attached to its parent, and this may be important if we
// want to remount the parent, because the vdom need to match the
// actual DOM
child.__destroy(childOwl.parent);
if (childOwl.pvnode) {
// we remove the key here to make sure that the patching algorithm
// is able to make the difference between this pvnode and an eventual
// other instance of the same component
delete childOwl.pvnode.key;
// Since the component has been unmounted, we do not want to actually
// call a remove hook. This is pretty important, since the t-component
// directive actually disabled it, so the vdom algorithm will just
// not remove the child elm if we don't remove the hook.
delete childOwl.pvnode.data!.hook!.remove;
}
} }
} }
if (!vnode) { if (!vnode) {
@@ -707,3 +766,34 @@ export class Component<Props extends {} = any, T extends Env = Env> {
} }
} }
} }
interface MountParameters {
env?: Env;
target: HTMLElement | DocumentFragment;
props?: any;
position?: MountOptions["position"];
}
interface Type<T> extends Function {
new (...args: any[]): T;
}
export async function mount<T extends Type<Component>>(
C: T,
params: MountParameters
): Promise<InstanceType<T>> {
const { env, props, target } = params;
let origEnv = C.hasOwnProperty("env") ? (C as any).env : null;
if (env) {
((C as any) as typeof Component).env = env;
}
const component: Component = new C(null, props);
if (origEnv) {
(C as any).env = origEnv;
} else {
delete (C as any).env;
}
const position = params.position || "last-child";
await component.mount(target, { position });
return component as any;
}
+40 -21
View File
@@ -7,7 +7,7 @@ import { makeHandlerCode, MODS_CODE } from "../qweb/extensions";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, { const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
self: "if (e.target !== vn.elm) {return}" self: "if (e.target !== vn.elm) {return}",
}); });
QWeb.utils.defineProxy = function defineProxy(target, source) { QWeb.utils.defineProxy = function defineProxy(target, source) {
@@ -18,7 +18,7 @@ QWeb.utils.defineProxy = function defineProxy(target, source) {
}, },
set(val) { set(val) {
source[k] = val; source[k] = val;
} },
}); });
} }
}; };
@@ -227,7 +227,9 @@ QWeb.addDirective({
if (name.startsWith("t-on-")) { if (name.startsWith("t-on-")) {
events.push([name, value]); events.push([name, value]);
} else if (name === "t-transition") { } else if (name === "t-transition") {
if (QWeb.enableTransitions) {
transition = value; transition = value;
}
} else if (!name.startsWith("t-")) { } else if (!name.startsWith("t-")) {
if (name !== "class" && name !== "style") { if (name !== "class" && name !== "style") {
// this is a prop! // this is a prop!
@@ -238,7 +240,7 @@ QWeb.addDirective({
// computing the props string representing the props object // computing the props string representing the props object
let propStr = Object.keys(props) let propStr = Object.keys(props)
.map(k => k + ":" + props[k]) .map((k) => k + ":" + props[k])
.join(","); .join(",");
let componentID = ctx.generateID(); let componentID = ctx.generateID();
@@ -280,7 +282,7 @@ QWeb.addDirective({
let classDef = classAttr let classDef = classAttr
.trim() .trim()
.split(/\s+/) .split(/\s+/)
.map(a => `'${a}':true`) .map((a) => `'${a}':true`)
.join(","); .join(",");
classObj = `_${ctx.generateID()}`; classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`); ctx.addLine(`let ${classObj} = {${classDef}};`);
@@ -298,7 +300,7 @@ QWeb.addDirective({
} }
} }
let eventsCode = events let eventsCode = events
.map(function([name, value]) { .map(function ([name, value]) {
const capture = name.match(/\.capture/); const capture = name.match(/\.capture/);
name = capture ? name.replace(/\.capture/, "") : name; name = capture ? name.replace(/\.capture/, "") : name;
const { event, handler } = makeHandlerCode( const { event, handler } = makeHandlerCode(
@@ -400,7 +402,6 @@ QWeb.addDirective({
if (hasSlots) { if (hasSlots) {
const clone = <Element>node.cloneNode(true); const clone = <Element>node.cloneNode(true);
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
// The next code is a fallback for compatibility reason. It accepts t-set // The next code is a fallback for compatibility reason. It accepts t-set
// elements that are direct children with a non empty body as nodes defining // elements that are direct children with a non empty body as nodes defining
@@ -410,27 +411,45 @@ QWeb.addDirective({
// code using slots. This will be removed in v2.0 someday. Meanwhile, // code using slots. This will be removed in v2.0 someday. Meanwhile,
// please use t-set-slot everywhere you need to set the content of a // please use t-set-slot everywhere you need to set the content of a
// slot. // slot.
for (let el of clone.children) { for (let node of clone.children) {
if (el.getAttribute("t-set") && el.hasChildNodes()) { if (node.hasAttribute("t-set") && node.hasChildNodes()) {
slotNodes.push(el); node.setAttribute("t-set-slot", node.getAttribute("t-set")!);
node.removeAttribute("t-set");
} }
} }
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
const slotNames = new Set<string>();
const slotId = QWeb.nextSlotId++; const slotId = QWeb.nextSlotId++;
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`); ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
if (slotNodes.length) { if (slotNodes.length) {
for (let i = 0, length = slotNodes.length; i < length; i++) { for (let i = 0, length = slotNodes.length; i < length; i++) {
const slotNode = slotNodes[i]; const slotNode = slotNodes[i];
slotNode.parentElement!.removeChild(slotNode); // check if this is defined in a sub component (in which case it should
let key = slotNode.getAttribute("t-set-slot")!; // be ignored)
slotNode.removeAttribute("t-set-slot"); let el = slotNode.parentElement;
let isInSubComponent = false;
// here again, this code should be removed when we stop supporting while (el !== clone) {
// using t-set to define the content of named slots. if (
if (!key) { el!.hasAttribute("t-component") ||
key = slotNode.getAttribute("t-set")!; el!.tagName[0] === el!.tagName[0].toUpperCase()
slotNode.removeAttribute("t-set"); ) {
isInSubComponent = true;
break;
} }
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx); el = el.parentElement;
}
if (isInSubComponent) {
continue;
}
let key = slotNode.getAttribute("t-set-slot")!;
if (slotNames.has(key)) {
continue;
}
slotNames.add(key);
slotNode.removeAttribute("t-set-slot");
slotNode.parentElement!.removeChild(slotNode);
const slotFn = qweb._compile(`slot_${key}_template`, { elem: slotNode, hasParent: true });
QWeb.slots[`${slotId}_${key}`] = slotFn; QWeb.slots[`${slotId}_${key}`] = slotFn;
} }
} }
@@ -439,7 +458,7 @@ QWeb.addDirective({
for (let child of Object.values(clone.childNodes)) { for (let child of Object.values(clone.childNodes)) {
t.appendChild(child); t.appendChild(child);
} }
const slotFn = qweb._compile(`slot_default_template`, t, ctx); const slotFn = qweb._compile(`slot_default_template`, { elem: t, hasParent: true });
QWeb.slots[`${slotId}_default`] = slotFn; QWeb.slots[`${slotId}_default`] = slotFn;
} }
} }
@@ -469,5 +488,5 @@ QWeb.addDirective({
ctx.addLine(`w${componentID}.__owl__.parentLastFiberId = extra.fiber.id;`); ctx.addLine(`w${componentID}.__owl__.parentLastFiberId = extra.fiber.id;`);
return true; return true;
} },
}); });
+10 -2
View File
@@ -193,7 +193,7 @@ export class Fiber {
// build patchQueue // build patchQueue
const patchQueue: Fiber[] = []; const patchQueue: Fiber[] = [];
const doWork: (Fiber) => Fiber | null = function(f) { const doWork: (Fiber) => Fiber | null = function (f) {
patchQueue.push(f); patchQueue.push(f);
return f.child; return f.child;
}; };
@@ -238,13 +238,21 @@ export class Fiber {
} else { } else {
if (fiber.shouldPatch) { if (fiber.shouldPatch) {
component.__patch(component.__owl__.vnode!, fiber.vnode!); component.__patch(component.__owl__.vnode!, fiber.vnode!);
// When updating a Component's props (in directive),
// the component has a pvnode AND should be patched.
// However, its pvnode.elm may have changed if it is a High Order Component
if (component.__owl__.pvnode) {
component.__owl__.pvnode.elm = component.__owl__.vnode!.elm;
}
} else { } else {
component.__patch(document.createElement(fiber.vnode!.sel!), fiber.vnode!); component.__patch(document.createElement(fiber.vnode!.sel!), fiber.vnode!);
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm; component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
} }
} }
if (fiber === component.__owl__.currentFiber) {
component.__owl__.currentFiber = null; component.__owl__.currentFiber = null;
} }
}
// insert into the DOM (mount case) // insert into the DOM (mount case)
let inDOM = false; let inDOM = false;
@@ -280,7 +288,7 @@ export class Fiber {
* Cancel a fiber and all its children. * Cancel a fiber and all its children.
*/ */
cancel() { cancel() {
this._walk(f => { this._walk((f) => {
if (!f.isRendered) { if (!f.isRendered) {
f.root.counter--; f.root.counter--;
} }
+2 -2
View File
@@ -11,7 +11,7 @@ import { QWeb } from "../qweb/index";
* This is why it is only done in 'dev' mode. * This is why it is only done in 'dev' mode.
*/ */
QWeb.utils.validateProps = function(Widget, props: Object) { QWeb.utils.validateProps = function (Widget, props: Object) {
const propsDef = (<any>Widget).props; const propsDef = (<any>Widget).props;
if (propsDef instanceof Array) { if (propsDef instanceof Array) {
// list of strings (prop names) // list of strings (prop names)
@@ -37,7 +37,7 @@ QWeb.utils.validateProps = function(Widget, props: Object) {
if (propsDef[propName] && !propsDef[propName].optional) { if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(`Missing props '${propName}' (component '${Widget.name}')`); throw new Error(`Missing props '${propName}' (component '${Widget.name}')`);
} else { } else {
break; continue;
} }
} }
let isValid; let isValid;
+3 -3
View File
@@ -50,7 +50,7 @@ export class Scheduler {
return reject(fiber.error); return reject(fiber.error);
} }
resolve(); resolve();
} },
}); });
if (!this.isRunning) { if (!this.isRunning) {
this.start(); this.start();
@@ -60,7 +60,7 @@ export class Scheduler {
rejectFiber(fiber: Fiber, reason: string) { rejectFiber(fiber: Fiber, reason: string) {
fiber = fiber.root; fiber = fiber.root;
const index = this.tasks.findIndex(t => t.fiber === fiber); const index = this.tasks.findIndex((t) => t.fiber === fiber);
if (index >= 0) { if (index >= 0) {
const [task] = this.tasks.splice(index, 1); const [task] = this.tasks.splice(index, 1);
fiber.cancel(); fiber.cancel();
@@ -76,7 +76,7 @@ export class Scheduler {
flush() { flush() {
let tasks = this.tasks; let tasks = this.tasks;
this.tasks = []; this.tasks = [];
tasks = tasks.filter(task => { tasks = tasks.filter((task) => {
if (task.fiber.isCompleted) { if (task.fiber.isCompleted) {
task.callback(); task.callback();
return false; return false;
+1 -1
View File
@@ -8,7 +8,7 @@
export const STYLESHEETS: { [id: string]: HTMLStyleElement } = {}; export const STYLESHEETS: { [id: string]: HTMLStyleElement } = {};
export function processSheet(str: string): string { export function processSheet(str: string): string {
const tokens = str.split(/(\{|\}|;)/).map(s => s.trim()); const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
const selectorStack: string[][] = []; const selectorStack: string[][] = [];
const parts: string[] = []; const parts: string[] = [];
let rules: string[] = []; let rules: string[] = [];
+11 -1
View File
@@ -8,6 +8,7 @@ import { QWeb } from "./qweb/index";
interface Config { interface Config {
mode: string; mode: string;
enableTransitions: boolean;
} }
export const config = {} as Config; export const config = {} as Config;
@@ -26,5 +27,14 @@ Object.defineProperty(config, "mode", {
} else { } else {
console.log(`Owl is now running in 'prod' mode.`); console.log(`Owl is now running in 'prod' mode.`);
} }
} },
});
Object.defineProperty(config, "enableTransitions", {
get() {
return QWeb.enableTransitions;
},
set(value: boolean) {
QWeb.enableTransitions = value;
},
}); });
+6 -6
View File
@@ -79,9 +79,9 @@ export class Context extends EventBus {
async __notifyComponents() { async __notifyComponents() {
const rev = ++this.rev; const rev = ++this.rev;
const subscriptions = this.subscriptions.update; const subscriptions = this.subscriptions.update;
const groups = partitionBy(subscriptions, s => (s.owner ? s.owner.__owl__.depth : -1)); const groups = partitionBy(subscriptions, (s) => (s.owner ? s.owner.__owl__.depth : -1));
for (let group of groups) { for (let group of groups) {
const proms = group.map(sub => sub.callback.call(sub.owner, rev)); const proms = group.map((sub) => sub.callback.call(sub.owner, rev));
// at this point, each component in the current group has registered a // at this point, each component in the current group has registered a
// top level fiber in the scheduler. It could happen that rendering these // top level fiber in the scheduler. It could happen that rendering these
// components is done (if they have no children). This is why we manually // components is done (if they have no children). This is why we manually
@@ -116,7 +116,7 @@ export function useContextWithCB(ctx: Context, component: Component, method): an
__owl__.observer.notifyCB = component.render.bind(component); __owl__.observer.notifyCB = component.render.bind(component);
} }
const currentCB = __owl__.observer.notifyCB; const currentCB = __owl__.observer.notifyCB;
__owl__.observer.notifyCB = function() { __owl__.observer.notifyCB = function () {
if (ctx.rev > mapping[id]) { if (ctx.rev > mapping[id]) {
// in this case, the context has been updated since we were rendering // in this case, the context has been updated since we were rendering
// last, and we do not need to render here with the observer. A // last, and we do not need to render here with the observer. A
@@ -128,18 +128,18 @@ export function useContextWithCB(ctx: Context, component: Component, method): an
mapping[id] = 0; mapping[id] = 0;
const renderFn = __owl__.renderFn; const renderFn = __owl__.renderFn;
__owl__.renderFn = function(comp, params) { __owl__.renderFn = function (comp, params) {
mapping[id] = ctx.rev; mapping[id] = ctx.rev;
return renderFn(comp, params); return renderFn(comp, params);
}; };
ctx.on("update", component, async contextRev => { ctx.on("update", component, async (contextRev) => {
if (mapping[id] < contextRev) { if (mapping[id] < contextRev) {
mapping[id] = contextRev; mapping[id] = contextRev;
await method(); await method();
} }
}); });
const __destroy = component.__destroy; const __destroy = component.__destroy;
component.__destroy = parent => { component.__destroy = (parent) => {
ctx.off("update", component); ctx.off("update", component);
delete mapping[id]; delete mapping[id];
__destroy.call(component, parent); __destroy.call(component, parent);
+2 -2
View File
@@ -44,7 +44,7 @@ export class EventBus {
} }
this.subscriptions[eventType].push({ this.subscriptions[eventType].push({
owner, owner,
callback callback,
}); });
} }
@@ -54,7 +54,7 @@ export class EventBus {
off(eventType: string, owner: any) { off(eventType: string, owner: any) {
const subs = this.subscriptions[eventType]; const subs = this.subscriptions[eventType];
if (subs) { if (subs) {
this.subscriptions[eventType] = subs.filter(s => s.owner !== owner); this.subscriptions[eventType] = subs.filter((s) => s.owner !== owner);
} }
} }
+8 -4
View File
@@ -21,11 +21,15 @@ export class Observer {
rev: number = 1; rev: number = 1;
allowMutations: boolean = true; allowMutations: boolean = true;
weakMap: WeakMap<any, any> = new WeakMap(); weakMap: WeakMap<any, any> = new WeakMap();
notifyCB() {} notifyCB() {}
observe<T>(value: T, parent?: any): T { observe<T>(value: T, parent?: any): T {
if (value === null || typeof value !== "object" || value instanceof Date) { if (
value === null ||
typeof value !== "object" ||
value instanceof Date ||
value instanceof Promise
) {
// fun fact: typeof null === 'object' // fun fact: typeof null === 'object'
return value; return value;
} }
@@ -67,14 +71,14 @@ export class Observer {
self.notifyCB(); self.notifyCB();
} }
return true; return true;
} },
}); });
const metadata = { const metadata = {
value, value,
proxy, proxy,
rev: this.rev, rev: this.rev,
parent parent,
}; };
this.weakMap.set(value, metadata); this.weakMap.set(value, metadata);
+12 -12
View File
@@ -36,11 +36,11 @@ export function useState<T>(state: T): T {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
function makeLifecycleHook(method: string, reverse: boolean = false) { function makeLifecycleHook(method: string, reverse: boolean = false) {
if (reverse) { if (reverse) {
return function(cb) { return function (cb) {
const component: Component = Component.current!; const component: Component = Component.current!;
if (component.__owl__[method]) { if (component.__owl__[method]) {
const current = component.__owl__[method]; const current = component.__owl__[method];
component.__owl__[method] = function() { component.__owl__[method] = function () {
current.call(component); current.call(component);
cb.call(component); cb.call(component);
}; };
@@ -49,11 +49,11 @@ function makeLifecycleHook(method: string, reverse: boolean = false) {
} }
}; };
} else { } else {
return function(cb) { return function (cb) {
const component: Component = Component.current!; const component: Component = Component.current!;
if (component.__owl__[method]) { if (component.__owl__[method]) {
const current = component.__owl__[method]; const current = component.__owl__[method];
component.__owl__[method] = function() { component.__owl__[method] = function () {
cb.call(component); cb.call(component);
current.call(component); current.call(component);
}; };
@@ -65,11 +65,11 @@ function makeLifecycleHook(method: string, reverse: boolean = false) {
} }
function makeAsyncHook(method: string) { function makeAsyncHook(method: string) {
return function(cb) { return function (cb) {
const component: Component = Component.current!; const component: Component = Component.current!;
if (component.__owl__[method]) { if (component.__owl__[method]) {
const current = component.__owl__[method]; const current = component.__owl__[method];
component.__owl__[method] = function(...args) { component.__owl__[method] = function (...args) {
return Promise.all([current.call(component, ...args), cb.call(component, ...args)]); return Promise.all([current.call(component, ...args), cb.call(component, ...args)]);
}; };
} else { } else {
@@ -94,12 +94,12 @@ export const onWillUpdateProps = makeAsyncHook("willUpdatePropsCB");
* The purpose of this hook is to allow components to get a reference to a sub * The purpose of this hook is to allow components to get a reference to a sub
* html node or component. * html node or component.
*/ */
interface Ref { interface Ref<C extends Component = Component> {
el: HTMLElement | null; el: HTMLElement | null;
comp: Component | null; comp: C | null;
} }
export function useRef(name: string): Ref { export function useRef<C extends Component = Component>(name: string): Ref<C> {
const __owl__ = Component.current!.__owl__; const __owl__ = Component.current!.__owl__;
return { return {
get el(): HTMLElement | null { get el(): HTMLElement | null {
@@ -111,10 +111,10 @@ export function useRef(name: string): Ref {
} }
return null; return null;
}, },
get comp(): Component | null { get comp(): C | null {
const val = __owl__.refs && __owl__.refs[name]; const val = __owl__.refs && __owl__.refs[name];
return val instanceof Component ? val : null; return val instanceof Component ? (val as C) : null;
} },
}; };
} }
+3 -2
View File
@@ -19,7 +19,7 @@ import { Link } from "./router/link";
import { RouteComponent } from "./router/route_component"; import { RouteComponent } from "./router/route_component";
import { Router } from "./router/router"; import { Router } from "./router/router";
export { Component } from "./component/component"; export { Component, mount } from "./component/component";
export { QWeb }; export { QWeb };
export { config }; export { config };
@@ -35,6 +35,7 @@ export const hooks = Object.assign({}, _hooks, {
useContext: _context.useContext, useContext: _context.useContext,
useDispatch: _store.useDispatch, useDispatch: _store.useDispatch,
useGetters: _store.useGetters, useGetters: _store.useGetters,
useStore: _store.useStore useStore: _store.useStore,
}); });
export const __info__ = {}; export const __info__ = {};
+4 -4
View File
@@ -26,8 +26,8 @@ export class Portal extends Component<Props> {
static template = xml`<portal><t t-slot="default"/></portal>`; static template = xml`<portal><t t-slot="default"/></portal>`;
static props = { static props = {
target: { target: {
type: String type: String,
} },
}; };
// boolean to indicate whether or not we must listen to 'dom-appended' event // boolean to indicate whether or not we must listen to 'dom-appended' event
@@ -55,12 +55,12 @@ export class Portal extends Component<Props> {
// put a callback in the env that is propagated to children s.t. portal can // put a callback in the env that is propagated to children s.t. portal can
// register an handler to those events just before children will trigger them // register an handler to those events just before children will trigger them
useSubEnv({ useSubEnv({
[portalSymbol]: ev => { [portalSymbol]: (ev) => {
if (!this._handledEvents.has(ev.type)) { if (!this._handledEvents.has(ev.type)) {
this.portal!.elm!.addEventListener(ev.type, this._handlerTunnel); this.portal!.elm!.addEventListener(ev.type, this._handlerTunnel);
this._handledEvents.add(ev.type); this._handledEvents.add(ev.type);
} }
} },
}); });
} }
/** /**
+41 -29
View File
@@ -1,4 +1,4 @@
import { CompilationContext } from "./compilation_context"; import { CompilationContext, INTERP_REGEXP } from "./compilation_context";
import { QWeb } from "./qweb"; import { QWeb } from "./qweb";
import { htmlToVDOM } from "../vdom/html_to_vdom"; import { htmlToVDOM } from "../vdom/html_to_vdom";
import { QWebVar } from "./expression_parser"; import { QWebVar } from "./expression_parser";
@@ -49,6 +49,7 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
if (ctx.escaping) { if (ctx.escaping) {
let protectID; let protectID;
if (value.hasBody) { if (value.hasBody) {
ctx.rootContext.shouldDefineUtils = true;
protectID = ctx.startProtectScope(); protectID = ctx.startProtectScope();
ctx.addLine( ctx.addLine(
`${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};` `${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};`
@@ -96,7 +97,7 @@ QWeb.addDirective({
let value = ctx.getValue(node.getAttribute("t-esc")!); let value = ctx.getValue(node.getAttribute("t-esc")!);
compileValueNode(value, node, qweb, ctx.subContext("escaping", true)); compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
return true; return true;
} },
}); });
QWeb.addDirective({ QWeb.addDirective({
@@ -106,7 +107,7 @@ QWeb.addDirective({
let value = ctx.getValue(node.getAttribute("t-raw")!); let value = ctx.getValue(node.getAttribute("t-raw")!);
compileValueNode(value, node, qweb, ctx); compileValueNode(value, node, qweb, ctx);
return true; return true;
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -163,7 +164,7 @@ QWeb.addDirective({
} }
} }
return true; return true;
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -179,7 +180,7 @@ QWeb.addDirective({
}, },
finalize({ ctx }) { finalize({ ctx }) {
ctx.closeIf(); ctx.closeIf();
} },
}); });
QWeb.addDirective({ QWeb.addDirective({
@@ -195,7 +196,7 @@ QWeb.addDirective({
}, },
finalize({ ctx }) { finalize({ ctx }) {
ctx.closeIf(); ctx.closeIf();
} },
}); });
QWeb.addDirective({ QWeb.addDirective({
@@ -208,7 +209,7 @@ QWeb.addDirective({
}, },
finalize({ ctx }) { finalize({ ctx }) {
ctx.closeIf(); ctx.closeIf();
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -222,77 +223,88 @@ QWeb.addDirective({
// ------------------------------------------------ // ------------------------------------------------
ctx.rootContext.shouldDefineScope = true; ctx.rootContext.shouldDefineScope = true;
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
if (node.nodeName !== "t") {
throw new Error("Invalid tag for t-call directive (should be 't')");
}
const subTemplate = node.getAttribute("t-call")!; const subTemplate = node.getAttribute("t-call")!;
const isDynamic = INTERP_REGEXP.test(subTemplate);
const nodeTemplate = qweb.templates[subTemplate]; const nodeTemplate = qweb.templates[subTemplate];
if (!nodeTemplate) { if (!isDynamic && !nodeTemplate) {
throw new Error(`Cannot find template "${subTemplate}" (t-call)`); throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
} }
// Step 2: compile target template in sub templates // Step 2: compile target template in sub templates
// ------------------------------------------------ // ------------------------------------------------
if (!qweb.subTemplates[subTemplate]) { let subIdstr: string;
qweb.subTemplates[subTemplate] = true; if (isDynamic) {
const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx, true); const _id = ctx.generateID();
qweb.subTemplates[subTemplate] = subTemplateFn; ctx.addLine(`let tname${_id} = ${ctx.interpolate(subTemplate)};`);
ctx.addLine(`let tid${_id} = this.subTemplates[tname${_id}];`);
ctx.addIf(`!tid${_id}`);
ctx.addLine(`tid${_id} = this.constructor.nextId++;`);
ctx.addLine(`this.subTemplates[tname${_id}] = tid${_id};`);
ctx.addLine(
`this.constructor.subTemplates[tid${_id}] = this._compile(tname${_id}, {hasParent: true, defineKey: true});`
);
ctx.closeIf();
subIdstr = `tid${_id}`;
} else {
let subId = qweb.subTemplates[subTemplate];
if (!subId) {
subId = QWeb.nextId++;
qweb.subTemplates[subTemplate] = subId;
const subTemplateFn = qweb._compile(subTemplate, { hasParent: true, defineKey: true });
QWeb.subTemplates[subId] = subTemplateFn;
}
subIdstr = `'${subId}'`;
} }
// Step 3: compile t-call body if necessary // Step 3: compile t-call body if necessary
// ------------------------------------------------ // ------------------------------------------------
let hasBody = node.hasChildNodes(); let hasBody = node.hasChildNodes();
let protectID; const protectID = ctx.startProtectScope();
if (hasBody) { if (hasBody) {
// we add a sub scope to protect the ambient scope // we add a sub scope to protect the ambient scope
ctx.addLine(`{`); ctx.addLine(`{`);
ctx.indent(); ctx.indent();
protectID = ctx.startProtectScope();
const nodeCopy = node.cloneNode(true) as Element; const nodeCopy = node.cloneNode(true) as Element;
for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) { for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) {
nodeCopy.removeAttribute(attr); nodeCopy.removeAttribute(attr);
} }
const parentNode = ctx.parentNode;
ctx.parentNode = "__0";
// this local scope is intended to trap c__0 // this local scope is intended to trap c__0
ctx.addLine(`{`); ctx.addLine(`{`);
ctx.indent(); ctx.indent();
ctx.addLine("let c__0 = [];"); ctx.addLine("let c__0 = [];");
qweb._compileNode(nodeCopy, ctx); qweb._compileNode(nodeCopy, ctx.subContext("parentNode", "__0"));
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
ctx.addLine("scope[utils.zero] = c__0;"); ctx.addLine("scope[utils.zero] = c__0;");
ctx.parentNode = parentNode;
ctx.dedent(); ctx.dedent();
ctx.addLine(`}`); ctx.addLine(`}`);
} }
// Step 4: add the appropriate function call to current component // Step 4: add the appropriate function call to current component
// ------------------------------------------------ // ------------------------------------------------
const callingScope = hasBody ? "scope" : "Object.assign(Object.create(context), scope)";
const parentComponent = `utils.getComponent(context)`; const parentComponent = `utils.getComponent(context)`;
const key = ctx.generateTemplateKey(); const key = ctx.generateTemplateKey();
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result"; const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`; const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`;
if (ctx.parentNode) { if (ctx.parentNode) {
ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, ${extra});`); ctx.addLine(`this.constructor.subTemplates[${subIdstr}].call(this, scope, ${extra});`);
} else { } else {
// this is a t-call with no parentnode, we need to extract the result // this is a t-call with no parentnode, we need to extract the result
ctx.rootContext.shouldDefineResult = true; ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`result = []`); ctx.addLine(`result = []`);
ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, ${extra});`); ctx.addLine(`this.constructor.subTemplates[${subIdstr}].call(this, scope, ${extra});`);
ctx.addLine(`result = result[0]`); ctx.addLine(`result = result[0]`);
} }
// Step 5: restore previous scope // Step 5: restore previous scope
// ------------------------------------------------ // ------------------------------------------------
if (hasBody) { if (hasBody) {
ctx.stopProtectScope(protectID);
ctx.dedent(); ctx.dedent();
ctx.addLine(`}`); ctx.addLine(`}`);
} }
ctx.stopProtectScope(protectID);
return true; return true;
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -353,7 +365,7 @@ QWeb.addDirective({
ctx.addLine("}"); ctx.addLine("}");
ctx.stopProtectScope(varsID); ctx.stopProtectScope(varsID);
return true; return true;
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -364,7 +376,7 @@ QWeb.addDirective({
priority: 1, priority: 1,
atNodeEncounter({ ctx }) { atNodeEncounter({ ctx }) {
ctx.addLine("debugger;"); ctx.addLine("debugger;");
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -376,5 +388,5 @@ QWeb.addDirective({
atNodeEncounter({ ctx, value }) { atNodeEncounter({ ctx, value }) {
const expr = ctx.formatExpression(value); const expr = ctx.formatExpression(value);
ctx.addLine(`console.log(${expr})`); ctx.addLine(`console.log(${expr})`);
} },
}); });
+2 -2
View File
@@ -162,7 +162,7 @@ export class CompilationContext {
const tokens = compileExprToArray(expr, this.variables); const tokens = compileExprToArray(expr, this.variables);
const done = new Set(); const done = new Set();
return tokens return tokens
.map(tok => { .map((tok) => {
if (tok.varName) { if (tok.varName) {
if (!done.has(tok.varName)) { if (!done.has(tok.varName)) {
done.add(tok.varName); done.add(tok.varName);
@@ -189,7 +189,7 @@ export class CompilationContext {
return `(${this.formatExpression(s.slice(2, -2))})`; return `(${this.formatExpression(s.slice(2, -2))})`;
} }
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}"); let r = s.replace(/\{\{.*?\}\}/g, (s) => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`"; return "`" + r + "`";
} }
startProtectScope(codeBlock?: boolean): number { startProtectScope(codeBlock?: boolean): number {
+9 -9
View File
@@ -35,7 +35,7 @@ const WORD_REPLACEMENT = {
gt: ">", gt: ">",
gte: ">=", gte: ">=",
lt: "<", lt: "<",
lte: "<=" lte: "<=",
}; };
export interface QWebVar { export interface QWebVar {
@@ -77,7 +77,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
":": "COLON", ":": "COLON",
",": "COMMA", ",": "COMMA",
"(": "LEFT_PAREN", "(": "LEFT_PAREN",
")": "RIGHT_PAREN" ")": "RIGHT_PAREN",
}; };
// note that the space after typeof is relevant. It makes sure that the formatted // note that the space after typeof is relevant. It makes sure that the formatted
@@ -86,7 +86,7 @@ const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,
type Tokenizer = (expr: string) => Token | false; type Tokenizer = (expr: string) => Token | false;
let tokenizeString: Tokenizer = function(expr) { let tokenizeString: Tokenizer = function (expr) {
let s = expr[0]; let s = expr[0];
let start = s; let start = s;
if (s !== "'" && s !== '"') { if (s !== "'" && s !== '"') {
@@ -114,7 +114,7 @@ let tokenizeString: Tokenizer = function(expr) {
return { type: "VALUE", value: s }; return { type: "VALUE", value: s };
}; };
let tokenizeNumber: Tokenizer = function(expr) { let tokenizeNumber: Tokenizer = function (expr) {
let s = expr[0]; let s = expr[0];
if (s && s.match(/[0-9]/)) { if (s && s.match(/[0-9]/)) {
let i = 1; let i = 1;
@@ -128,7 +128,7 @@ let tokenizeNumber: Tokenizer = function(expr) {
} }
}; };
let tokenizeSymbol: Tokenizer = function(expr) { let tokenizeSymbol: Tokenizer = function (expr) {
let s = expr[0]; let s = expr[0];
if (s && s.match(/[a-zA-Z_\$]/)) { if (s && s.match(/[a-zA-Z_\$]/)) {
let i = 1; let i = 1;
@@ -145,7 +145,7 @@ let tokenizeSymbol: Tokenizer = function(expr) {
} }
}; };
const tokenizeStatic: Tokenizer = function(expr) { const tokenizeStatic: Tokenizer = function (expr) {
const char = expr[0]; const char = expr[0];
if (char && char in STATIC_TOKEN_MAP) { if (char && char in STATIC_TOKEN_MAP) {
return { type: STATIC_TOKEN_MAP[char], value: char }; return { type: STATIC_TOKEN_MAP[char], value: char };
@@ -153,7 +153,7 @@ const tokenizeStatic: Tokenizer = function(expr) {
return false; return false;
}; };
const tokenizeOperator: Tokenizer = function(expr) { const tokenizeOperator: Tokenizer = function (expr) {
for (let op of OPERATORS) { for (let op of OPERATORS) {
if (expr.startsWith(op)) { if (expr.startsWith(op)) {
return { type: "OPERATOR", value: op }; return { type: "OPERATOR", value: op };
@@ -167,7 +167,7 @@ const TOKENIZERS = [
tokenizeNumber, tokenizeNumber,
tokenizeOperator, tokenizeOperator,
tokenizeSymbol, tokenizeSymbol,
tokenizeStatic tokenizeStatic,
]; ];
/** /**
@@ -284,6 +284,6 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
export function compileExpr(expr: string, scope: { [key: string]: QWebVar }): string { export function compileExpr(expr: string, scope: { [key: string]: QWebVar }): string {
return compileExprToArray(expr, scope) return compileExprToArray(expr, scope)
.map(t => t.value) .map((t) => t.value)
.join(""); .join("");
} }
+57 -23
View File
@@ -1,4 +1,5 @@
import { VNode } from "../vdom/index"; import { VNode } from "../vdom/index";
import { INTERP_REGEXP } from "./compilation_context";
import { QWeb } from "./qweb"; import { QWeb } from "./qweb";
/** /**
@@ -23,7 +24,7 @@ import { QWeb } from "./qweb";
export const MODS_CODE = { export const MODS_CODE = {
prevent: "e.preventDefault();", prevent: "e.preventDefault();",
self: "if (e.target !== this.elm) {return}", self: "if (e.target !== this.elm) {return}",
stop: "e.stopPropagation();" stop: "e.stopPropagation();",
}; };
interface HandlerInfo { interface HandlerInfo {
@@ -50,7 +51,7 @@ export function makeHandlerCode(
let code: string; let code: string;
// check if it is a method with no args, a method with args or an expression // check if it is a method with no args, a method with args or an expression
let args: string = ""; let args: string = "";
const name: string = value.replace(/\(.*\)/, function(_args) { const name: string = value.replace(/\(.*\)/, function (_args) {
args = _args.slice(1, -1); args = _args.slice(1, -1);
return ""; return "";
}); });
@@ -74,7 +75,7 @@ export function makeHandlerCode(
putInCache = false; putInCache = false;
code = ctx.captureExpression(value); code = ctx.captureExpression(value);
} }
const modCode = mods.map(mod => modcodes[mod]).join(""); const modCode = mods.map((mod) => modcodes[mod]).join("");
let handler = `function (e) {if (!context.__owl__.isMounted){return}${modCode}${code}}`; let handler = `function (e) {if (!context.__owl__.isMounted){return}${modCode}${code}}`;
if (putInCache) { if (putInCache) {
const key = ctx.generateTemplateKey(event); const key = ctx.generateTemplateKey(event);
@@ -90,7 +91,7 @@ QWeb.addDirective({
atNodeCreation({ ctx, fullName, value, nodeID }) { atNodeCreation({ ctx, fullName, value, nodeID }) {
const { event, handler } = makeHandlerCode(ctx, fullName, value, true); const { event, handler } = makeHandlerCode(ctx, fullName, value, true);
ctx.addLine(`p${nodeID}.on['${event}'] = ${handler};`); ctx.addLine(`p${nodeID}.on['${event}'] = ${handler};`);
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -105,17 +106,17 @@ QWeb.addDirective({
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`); ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`); addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`); addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`);
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-transition // t-transition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
QWeb.utils.nextFrame = function(cb: () => void) { QWeb.utils.nextFrame = function (cb: () => void) {
requestAnimationFrame(() => requestAnimationFrame(cb)); requestAnimationFrame(() => requestAnimationFrame(cb));
}; };
QWeb.utils.transitionInsert = function(vn: VNode, name: string) { QWeb.utils.transitionInsert = function (vn: VNode, name: string) {
const elm = <HTMLElement>vn.elm; const elm = <HTMLElement>vn.elm;
// remove potential duplicated vnode that is currently being removed, to // remove potential duplicated vnode that is currently being removed, to
// prevent from having twice the same node in the DOM during an animation // prevent from having twice the same node in the DOM during an animation
@@ -139,7 +140,7 @@ QWeb.utils.transitionInsert = function(vn: VNode, name: string) {
}); });
}; };
QWeb.utils.transitionRemove = function(vn: VNode, name: string, rm: () => void) { QWeb.utils.transitionRemove = function (vn: VNode, name: string, rm: () => void) {
const elm = <HTMLElement>vn.elm; const elm = <HTMLElement>vn.elm;
elm.setAttribute("data-owl-key", vn.key!); elm.setAttribute("data-owl-key", vn.key!);
@@ -205,16 +206,19 @@ QWeb.addDirective({
name: "transition", name: "transition",
priority: 96, priority: 96,
atNodeCreation({ ctx, value, addNodeHook }) { atNodeCreation({ ctx, value, addNodeHook }) {
if (!QWeb.enableTransitions) {
return;
}
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
let name = value; let name = value;
const hooks = { const hooks = {
insert: `utils.transitionInsert(vn, '${name}');`, insert: `utils.transitionInsert(vn, '${name}');`,
remove: `utils.transitionRemove(vn, '${name}', rm);` remove: `utils.transitionRemove(vn, '${name}', rm);`,
}; };
for (let hookName in hooks) { for (let hookName in hooks) {
addNodeHook(hookName, hooks[hookName]); addNodeHook(hookName, hooks[hookName]);
} }
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -225,8 +229,9 @@ QWeb.addDirective({
priority: 80, priority: 80,
atNodeEncounter({ ctx, value, node, qweb }): boolean { atNodeEncounter({ ctx, value, node, qweb }): boolean {
const slotKey = ctx.generateID(); const slotKey = ctx.generateID();
const valueExpr = value.match(INTERP_REGEXP) ? ctx.interpolate(value) : `'${value}'`;
ctx.addLine( ctx.addLine(
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];` `const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + ${valueExpr}];`
); );
ctx.addIf(`slot${slotKey}`); ctx.addIf(`slot${slotKey}`);
let parentNode = `c${ctx.parentNode}`; let parentNode = `c${ctx.parentNode}`;
@@ -251,17 +256,20 @@ QWeb.addDirective({
} }
ctx.closeIf(); ctx.closeIf();
return true; return true;
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-model // t-model
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
QWeb.utils.toNumber = function(val: string): number | string { QWeb.utils.toNumber = function (val: string): number | string {
const n = parseFloat(val); const n = parseFloat(val);
return isNaN(n) ? val : n; return isNaN(n) ? val : n;
}; };
const hasDotAtTheEnd = /\.[\w_]+\s*$/;
const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
QWeb.addDirective({ QWeb.addDirective({
name: "model", name: "model",
priority: 42, priority: 42,
@@ -270,15 +278,41 @@ QWeb.addDirective({
let handler; let handler;
let event = fullName.includes(".lazy") ? "change" : "input"; let event = fullName.includes(".lazy") ? "change" : "input";
// we keep here a reference to the "base expression" (if the expression // First step: we need to understand the structure of the expression, and
// is `t-model="some.expr.value", then the base expression is "some.expr"). // from it, extract a base expression (that we can capture, which is
// This is necessary so we can capture it in the handler closure. // important because it will be used in a handler later) and a formatted
let expr = ctx.formatExpression(value); // expression (which uses the captured base expression)
const index = expr.lastIndexOf("."); //
const baseExpr = expr.slice(0, index); // Also, we support 2 kinds of values: some.expr.value or some.expr[value]
ctx.addLine(`let expr${nodeID} = ${baseExpr};`); // For the first one, we have:
// - base expression = scope[some].expr
// - expression = exprX.value (where exprX is the var that captures the base expr)
// and for the expression with brackets:
// - base expression = scope[some].expr
// - expression = exprX[keyX] (where exprX is the var that captures the base expr
// and keyX captures scope[value])
let expr: string;
let baseExpr: string;
if (hasDotAtTheEnd.test(value)) {
// we manage the case where the expr has a dot: some.expr.value
const index = value.lastIndexOf(".");
baseExpr = value.slice(0, index);
ctx.addLine(`let expr${nodeID} = ${ctx.formatExpression(baseExpr)};`);
expr = `expr${nodeID}${value.slice(index)}`;
} else if (hasBracketsAtTheEnd.test(value)) {
// we manage here the case where the expr ends in a bracket expression:
// some.expr[value]
const index = value.lastIndexOf("[");
baseExpr = value.slice(0, index);
ctx.addLine(`let expr${nodeID} = ${ctx.formatExpression(baseExpr)};`);
let exprKey = value.trimRight().slice(index + 1, -1);
ctx.addLine(`let exprKey${nodeID} = ${ctx.formatExpression(exprKey)};`);
expr = `expr${nodeID}[exprKey${nodeID}]`;
} else {
throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
}
expr = `expr${nodeID}.${expr.slice(index + 1)}`;
const key = ctx.generateTemplateKey(); const key = ctx.generateTemplateKey();
if (node.tagName === "select") { if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`); ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
@@ -305,7 +339,7 @@ QWeb.addDirective({
} }
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`); ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`); ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`);
} },
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -329,5 +363,5 @@ QWeb.addDirective({
if (ctx.loopNumber === 0) { if (ctx.loopNumber === 0) {
ctx.rootContext.hasKey0 = ctx.keyStack.pop() as boolean; ctx.rootContext.hasKey0 = ctx.keyStack.pop() as boolean;
} }
} },
}); });
+87 -55
View File
@@ -66,8 +66,6 @@ interface QWebConfig {
// Const/global stuff/helpers // Const/global stuff/helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"]; const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const lineBreakRE = /[\r\n]/; const lineBreakRE = /[\r\n]/;
@@ -77,7 +75,7 @@ const NODE_HOOKS_PARAMS = {
create: "(_, n)", create: "(_, n)",
insert: "vn", insert: "vn",
remove: "(vn, rm)", remove: "(vn, rm)",
destroy: "()" destroy: "()",
}; };
interface Utils { interface Utils {
@@ -86,10 +84,30 @@ interface Utils {
[key: string]: any; [key: string]: any;
} }
function isComponent(obj) { function isComponent(obj): boolean {
return obj && obj.hasOwnProperty("__owl__"); return obj && obj.hasOwnProperty("__owl__");
} }
class VDomArray extends Array {
toString() {
return vDomToString(this);
}
}
function vDomToString(vdom: VNode[]): string {
return vdom
.map((vnode) => {
if (vnode.sel) {
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
} else {
return vnode.text;
}
})
.join("");
}
const UTILS: Utils = { const UTILS: Utils = {
zero: Symbol("zero"), zero: Symbol("zero"),
toObj(expr) { toObj(expr) {
@@ -111,20 +129,8 @@ const UTILS: Utils = {
addNameSpace(vnode) { addNameSpace(vnode) {
addNS(vnode.data, vnode.children, vnode.sel); addNS(vnode.data, vnode.children, vnode.sel);
}, },
VDomArray: class VDomArray extends Array {}, VDomArray,
vDomToString: function(vdom: VNode[]): string { vDomToString,
return vdom
.map(vnode => {
if (vnode.sel) {
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
} else {
return vnode.text;
}
})
.join("");
},
getComponent(obj) { getComponent(obj) {
while (obj && !isComponent(obj)) { while (obj && !isComponent(obj)) {
obj = obj.__proto__; obj = obj.__proto__;
@@ -145,7 +151,7 @@ const UTILS: Utils = {
obj = newObj; obj = newObj;
} }
return obj; return obj;
} },
}; };
function parseXML(xml: string): Document { function parseXML(xml: string): Document {
@@ -195,7 +201,7 @@ export class QWeb extends EventBus {
name: 1, name: 1,
att: 1, att: 1,
attf: 1, attf: 1,
translation: 1 translation: 1,
}; };
static DIRECTIVES: Directive[] = []; static DIRECTIVES: Directive[] = [];
@@ -206,16 +212,19 @@ export class QWeb extends EventBus {
h = h; h = h;
// dev mode enables better error messages or more costly validations // dev mode enables better error messages or more costly validations
static dev: boolean = false; static dev: boolean = false;
static enableTransitions: boolean = true;
// slots contains sub templates defined with t-set inside t-component nodes, and // slots contains sub templates defined with t-set inside t-component nodes, and
// are meant to be used by the t-slot directive. // are meant to be used by the t-slot directive.
static slots = {}; static slots = {};
static nextSlotId = 1; static nextSlotId = 1;
// recursiveTemplates contains sub templates called with t-call, but which // subTemplates are stored in two objects: a (local) mapping from a name to an
// ends up in recursive situations. This is very similar to the slot situation, // id, and a (global) mapping from an id to the compiled function. This is
// as in we need to propagate the scope. // necessary to ensure that global templates can be called with more than one
subTemplates = {}; // QWeb instance.
subTemplates: { [key: string]: number } = {};
static subTemplates: { [id: number]: Function } = {};
isUpdating: boolean = false; isUpdating: boolean = false;
translateFn?: QWebConfig["translateFn"]; translateFn?: QWebConfig["translateFn"];
@@ -239,7 +248,7 @@ export class QWeb extends EventBus {
QWeb.DIRECTIVE_NAMES[directive.name] = 1; QWeb.DIRECTIVE_NAMES[directive.name] = 1;
QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority); QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
if (directive.extraNames) { if (directive.extraNames) {
directive.extraNames.forEach(n => (QWeb.DIRECTIVE_NAMES[n] = 1)); directive.extraNames.forEach((n) => (QWeb.DIRECTIVE_NAMES[n] = 1));
} }
} }
@@ -303,11 +312,11 @@ export class QWeb extends EventBus {
this._processTemplate(elem); this._processTemplate(elem);
const template = { const template = {
elem, elem,
fn: function(this: QWeb, context, extra) { fn: function (this: QWeb, context, extra) {
const compiledFunction = this._compile(name, elem); const compiledFunction = this._compile(name);
template.fn = compiledFunction; template.fn = compiledFunction;
return compiledFunction.call(this, context, extra); return compiledFunction.call(this, context, extra);
} },
}; };
this.templates[name] = template; this.templates[name] = template;
} }
@@ -317,10 +326,10 @@ export class QWeb extends EventBus {
for (let i = 0, ilen = tbranch.length; i < ilen; i++) { for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
let node = tbranch[i]; let node = tbranch[i];
let prevElem = node.previousElementSibling!; let prevElem = node.previousElementSibling!;
let pattr = function(name) { let pattr = function (name) {
return prevElem.getAttribute(name); return prevElem.getAttribute(name);
}; };
let nattr = function(name) { let nattr = function (name) {
return +!!node.getAttribute(name); return +!!node.getAttribute(name);
}; };
if (prevElem && (pattr("t-if") || pattr("t-elif"))) { if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
@@ -330,7 +339,7 @@ export class QWeb extends EventBus {
); );
} }
if ( if (
["t-if", "t-elif", "t-else"].map(nattr).reduce(function(a, b) { ["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
return a + b; return a + b;
}) > 1 }) > 1
) { ) {
@@ -409,30 +418,33 @@ export class QWeb extends EventBus {
_compile( _compile(
name: string, name: string,
elem: Element, options: {
parentContext?: CompilationContext, elem?: Element;
defineKey?: boolean hasParent?: boolean;
defineKey?: boolean;
} = {}
): CompiledTemplate { ): CompiledTemplate {
const elem = options.elem || this.templates[name].elem;
const isDebug = elem.attributes.hasOwnProperty("t-debug"); const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new CompilationContext(name); const ctx = new CompilationContext(name);
if (elem.tagName !== "t") { if (elem.tagName !== "t") {
ctx.shouldDefineResult = false; ctx.shouldDefineResult = false;
} }
if (parentContext) { if (options.hasParent) {
ctx.variables = Object.create(parentContext.variables); ctx.variables = Object.create(null);
ctx.parentNode = parentContext.parentNode || ctx.generateID(); ctx.parentNode = ctx.generateID();
ctx.allowMultipleRoots = true; ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true; ctx.hasParentWidget = true;
ctx.shouldDefineResult = false; ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`); ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
if (defineKey) { if (options.defineKey) {
ctx.addLine(`let key0 = extra.key || "";`); ctx.addLine(`let key0 = extra.key || "";`);
ctx.hasKey0 = true; ctx.hasKey0 = true;
} }
} }
this._compileNode(elem, ctx); this._compileNode(elem, ctx);
if (!parentContext) { if (!options.hasParent) {
if (ctx.shouldDefineResult) { if (ctx.shouldDefineResult) {
ctx.addLine(`return result;`); ctx.addLine(`return result;`);
} else { } else {
@@ -507,10 +519,17 @@ export class QWeb extends EventBus {
return; return;
} }
if (node.tagName !== "t" && node.hasAttribute("t-call")) {
const tCallNode = document.createElement("t");
tCallNode.setAttribute("t-call", node.getAttribute("t-call")!);
node.removeAttribute("t-call");
node.prepend(tCallNode);
}
const firstLetter = node.tagName[0]; const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) { if (firstLetter === firstLetter.toUpperCase()) {
// this is a component, we modify in place the xml document to change // this is a component, we modify in place the xml document to change
// <SomeComponent ... /> to <t t-component="SomeComponent" ... /> // <SomeComponent ... /> to <SomeComponent t-component="SomeComponent" ... />
node.setAttribute("t-component", node.tagName); node.setAttribute("t-component", node.tagName);
} else if (node.tagName !== "t" && node.hasAttribute("t-component")) { } else if (node.tagName !== "t" && node.hasAttribute("t-component")) {
throw new Error( throw new Error(
@@ -582,7 +601,7 @@ export class QWeb extends EventBus {
qweb: this, qweb: this,
ctx, ctx,
fullName, fullName,
value value,
}); });
if (isDone) { if (isDone) {
for (let { directive, value, fullName } of finalizers) { for (let { directive, value, fullName } of finalizers) {
@@ -597,7 +616,7 @@ export class QWeb extends EventBus {
let nodeID = this._compileGenericNode(node, ctx, withHandlers); let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID); ctx = ctx.withParent(nodeID);
let nodeHooks = {}; let nodeHooks = {};
let addNodeHook = function(hook, handler) { let addNodeHook = function (hook, handler) {
nodeHooks[hook] = nodeHooks[hook] || []; nodeHooks[hook] = nodeHooks[hook] || [];
nodeHooks[hook].push(handler); nodeHooks[hook].push(handler);
}; };
@@ -611,7 +630,7 @@ export class QWeb extends EventBus {
fullName, fullName,
value, value,
nodeID, nodeID,
addNodeHook addNodeHook,
}); });
} }
} }
@@ -664,22 +683,31 @@ export class QWeb extends EventBus {
const props: string[] = []; const props: string[] = [];
const tattrs: number[] = []; const tattrs: number[] = [];
function handleBooleanProps(key, val) { function handleProperties(key, val) {
let isProp = false; let isProp = false;
if (node.nodeName === "input" && key === "checked") { switch (node.nodeName) {
case "input":
let type = (<Element>node).getAttribute("type"); let type = (<Element>node).getAttribute("type");
if (type === "checkbox" || type === "radio") { if (type === "checkbox" || type === "radio") {
if (key === "checked" || key === "indeterminate") {
isProp = true; isProp = true;
} }
} }
if (node.nodeName === "option" && key === "selected") { if (key === "value" || key === "readonly" || key === "disabled") {
isProp = true; isProp = true;
} }
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) { break;
isProp = true; case "option":
} isProp = key === "selected" || key === "disabled";
if ((key === "readonly" && node.nodeName === "input") || node.nodeName === "textarea") { break;
isProp = true; case "textarea":
isProp = key === "readonly" || key === "disabled";
break;
case "button":
case "select":
case "optgroup":
isProp = key === "disabled";
break;
} }
if (isProp) { if (isProp) {
props.push(`${key}: _${val}`); props.push(`${key}: _${val}`);
@@ -702,11 +730,15 @@ export class QWeb extends EventBus {
if ((value = value.trim())) { if ((value = value.trim())) {
let classDef = value let classDef = value
.split(/\s+/) .split(/\s+/)
.map(a => `'${escapeQuotes(a)}':true`) .map((a) => `'${escapeQuotes(a)}':true`)
.join(","); .join(",");
if (classObj) {
ctx.addLine(`Object.assign(${classObj}, {${classDef}})`);
} else {
classObj = `_${ctx.generateID()}`; classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`); ctx.addLine(`let ${classObj} = {${classDef}};`);
} }
}
} else { } else {
ctx.addLine(`let _${attID} = '${escapeQuotes(value)}';`); ctx.addLine(`let _${attID} = '${escapeQuotes(value)}';`);
if (!name.match(/^[a-zA-Z]+$/)) { if (!name.match(/^[a-zA-Z]+$/)) {
@@ -714,7 +746,7 @@ export class QWeb extends EventBus {
name = '"' + name + '"'; name = '"' + name + '"';
} }
attrs.push(`${name}: _${attID}`); attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID); handleProperties(name, attID);
} }
} }
@@ -746,12 +778,12 @@ export class QWeb extends EventBus {
const attValueID = ctx.generateID(); const attValueID = ctx.generateID();
ctx.addLine(`let _${attValueID} = ${formattedValue};`); ctx.addLine(`let _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`; formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":")); const attrIndex = attrs.findIndex((att) => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1); attrs.splice(attrIndex, 1);
} }
ctx.addLine(`let _${attID} = ${formattedValue};`); ctx.addLine(`let _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`); attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID); handleProperties(attName, attID);
} }
} }
+3 -3
View File
@@ -97,7 +97,7 @@ export class Router {
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
async start() { async start() {
(this as any)._listener = ev => this._navigate(this.currentPath(), ev); (this as any)._listener = (ev) => this._navigate(this.currentPath(), ev);
window.addEventListener("popstate", (this as any)._listener); window.addEventListener("popstate", (this as any)._listener);
if (this.mode === "hash") { if (this.mode === "hash") {
window.addEventListener("hashchange", (this as any)._listener); window.addEventListener("hashchange", (this as any)._listener);
@@ -198,7 +198,7 @@ export class Router {
return { return {
type: "match", type: "match",
route: route, route: route,
params: params params: params,
}; };
} }
} }
@@ -223,7 +223,7 @@ export class Router {
const result = await route.beforeRouteEnter({ const result = await route.beforeRouteEnter({
env: this.env, env: this.env,
from: this.currentRoute, from: this.currentRoute,
to: route to: route,
}); });
if (result === false) { if (result === false) {
return { type: "cancelled" }; return { type: "cancelled" };
+7 -7
View File
@@ -53,7 +53,7 @@ export class Store extends Context {
if (config.getters) { if (config.getters) {
const firstArg = { const firstArg = {
state: this.state, state: this.state,
getters: this.getters getters: this.getters,
}; };
for (let g in config.getters) { for (let g in config.getters) {
this.getters[g] = config.getters[g].bind(this, firstArg); this.getters[g] = config.getters[g].bind(this, firstArg);
@@ -70,7 +70,7 @@ export class Store extends Context {
dispatch: this.dispatch.bind(this), dispatch: this.dispatch.bind(this),
env: this.env, env: this.env,
state: this.state, state: this.state,
getters: this.getters getters: this.getters,
}, },
...payload ...payload
); );
@@ -113,11 +113,11 @@ export function useStore(selector, options: SelectorOptions = {}): any {
} }
return false; return false;
} }
store.updateFunctions[componentId].push(function(): boolean { store.updateFunctions[componentId].push(function (): boolean {
return selectCompareUpdate(store!.state, component.props); return selectCompareUpdate(store!.state, component.props);
}); });
useContextWithCB(store, component, function(): Promise<void> | void { useContextWithCB(store, component, function (): Promise<void> | void {
let shouldRender = false; let shouldRender = false;
for (let fn of store.updateFunctions[componentId]) { for (let fn of store.updateFunctions[componentId]) {
shouldRender = fn() || shouldRender; shouldRender = fn() || shouldRender;
@@ -126,12 +126,12 @@ export function useStore(selector, options: SelectorOptions = {}): any {
return component.render(); return component.render();
} }
}); });
onWillUpdateProps(props => { onWillUpdateProps((props) => {
selectCompareUpdate(store.state, props); selectCompareUpdate(store.state, props);
}); });
const __destroy = component.__destroy; const __destroy = component.__destroy;
component.__destroy = parent => { component.__destroy = (parent) => {
delete store.updateFunctions[componentId]; delete store.updateFunctions[componentId];
__destroy.call(component, parent); __destroy.call(component, parent);
}; };
@@ -148,7 +148,7 @@ export function useStore(selector, options: SelectorOptions = {}): any {
}, },
has(target, k) { has(target, k) {
return k in result; return k in result;
} },
}); });
} }
+6 -6
View File
@@ -13,13 +13,13 @@
import { browser } from "./browser"; import { browser } from "./browser";
export function whenReady(fn?: any) { export function whenReady(fn?: any) {
return new Promise(function(resolve) { return new Promise(function (resolve) {
if (document.readyState !== "loading") { if (document.readyState !== "loading") {
resolve(); resolve();
} else { } else {
document.addEventListener("DOMContentLoaded", resolve, false); document.addEventListener("DOMContentLoaded", resolve, false);
} }
}).then(fn || function() {}); }).then(fn || function () {});
} }
const loadedScripts: { [key: string]: Promise<void> } = {}; const loadedScripts: { [key: string]: Promise<void> } = {};
@@ -28,14 +28,14 @@ export function loadJS(url: string): Promise<void> {
if (url in loadedScripts) { if (url in loadedScripts) {
return loadedScripts[url]; return loadedScripts[url];
} }
const promise: Promise<void> = new Promise(function(resolve, reject) { const promise: Promise<void> = new Promise(function (resolve, reject) {
const script = document.createElement("script"); const script = document.createElement("script");
script.type = "text/javascript"; script.type = "text/javascript";
script.src = url; script.src = url;
script.onload = function() { script.onload = function () {
resolve(); resolve();
}; };
script.onerror = function() { script.onerror = function () {
reject(`Error loading file '${url}'`); reject(`Error loading file '${url}'`);
}; };
const head = document.head || document.getElementsByTagName("head")[0]; const head = document.head || document.getElementsByTagName("head")[0];
@@ -75,7 +75,7 @@ export function escape(str: string | number | undefined): string {
*/ */
export function debounce(func: Function, wait: number, immediate?: boolean): Function { export function debounce(func: Function, wait: number, immediate?: boolean): Function {
let timeout; let timeout;
return function(this: any) { return function (this: any) {
const context = this; const context = this;
const args = arguments; const args = arguments;
function later() { function later() {
+9 -2
View File
@@ -1,4 +1,4 @@
import { VNode, h } from "./vdom"; import { VNode, h, addNS } from "./vdom";
const parser = new DOMParser(); const parser = new DOMParser();
@@ -13,6 +13,9 @@ export function htmlToVDOM(html: string): VNode[] {
function htmlToVNode(node: ChildNode): VNode { function htmlToVNode(node: ChildNode): VNode {
if (!(node instanceof Element)) { if (!(node instanceof Element)) {
if (node instanceof Comment) {
return h("!", node.textContent);
}
return { text: node.textContent! } as VNode; return { text: node.textContent! } as VNode;
} }
const attrs = {}; const attrs = {};
@@ -23,5 +26,9 @@ function htmlToVNode(node: ChildNode): VNode {
for (let c of node.childNodes) { for (let c of node.childNodes) {
children.push(htmlToVNode(c)); children.push(htmlToVNode(c));
} }
return h((node as Element).tagName, { attrs }, children); const vnode = h((node as Element).tagName, { attrs }, children);
if (vnode.sel === "svg") {
addNS(vnode.data, (vnode as any).children, vnode.sel);
}
return vnode;
} }
+3 -3
View File
@@ -33,7 +33,7 @@ function updateProps(oldVnode: VNode, vnode: VNode): void {
export const propsModule = { export const propsModule = {
create: updateProps, create: updateProps,
update: updateProps update: updateProps,
} as Module; } as Module;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -151,7 +151,7 @@ function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
export const eventListenersModule = { export const eventListenersModule = {
create: updateEventListeners, create: updateEventListeners,
update: updateEventListeners, update: updateEventListeners,
destroy: updateEventListeners destroy: updateEventListeners,
} as Module; } as Module;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -210,7 +210,7 @@ function updateAttrs(oldVnode: VNode, vnode: VNode): void {
export const attrsModule = { export const attrsModule = {
create: updateAttrs, create: updateAttrs,
update: updateAttrs update: updateAttrs,
} as Module; } as Module;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
+1 -1
View File
@@ -520,7 +520,7 @@ const htmlDomApi = {
parentNode, parentNode,
nextSibling, nextSibling,
tagName, tagName,
setTextContent setTextContent,
} as DOMAPI; } as DOMAPI;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
+20 -20
View File
@@ -1,15 +1,15 @@
import { Component, Env } from "../src/component/component"; import { Component, Env } from "../src/component/component";
import { useRef, useState } from "../src/hooks";
import { QWeb } from "../src/qweb/index"; import { QWeb } from "../src/qweb/index";
import { useState, useRef } from "../src/hooks";
import { xml } from "../src/tags"; import { xml } from "../src/tags";
import { import {
makeDeferred, makeDeferred,
makeTestFixture,
makeTestEnv, makeTestEnv,
makeTestFixture,
nextFrame,
patchNextFrame, patchNextFrame,
renderToDOM, renderToDOM,
unpatchNextFrame, unpatchNextFrame,
nextTick
} from "./helpers"; } from "./helpers";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -71,7 +71,7 @@ describe("animations", () => {
qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`); qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`);
let def = makeDeferred(); let def = makeDeferred();
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(node.className).toBe("chimay-enter chimay-enter-active"); expect(node.className).toBe("chimay-enter chimay-enter-active");
cb(); cb();
expect(node.className).toBe("chimay-enter-active chimay-enter-to"); expect(node.className).toBe("chimay-enter-active chimay-enter-to");
@@ -91,7 +91,7 @@ describe("animations", () => {
qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`); qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`);
let def = makeDeferred(); let def = makeDeferred();
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(node.className).toBe("chimay-enter chimay-enter-active"); expect(node.className).toBe("chimay-enter chimay-enter-active");
cb(); cb();
expect(node.className).toBe("chimay-enter-active chimay-enter-to"); expect(node.className).toBe("chimay-enter-active chimay-enter-to");
@@ -112,7 +112,7 @@ describe("animations", () => {
qweb.addTemplate("test", `<span t-transition="jupiler">blue</span>`); qweb.addTemplate("test", `<span t-transition="jupiler">blue</span>`);
let def = makeDeferred(); let def = makeDeferred();
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(node.className).toBe("jupiler-enter jupiler-enter-active"); expect(node.className).toBe("jupiler-enter jupiler-enter-active");
cb(); cb();
expect(node.className).toBe(""); expect(node.className).toBe("");
@@ -139,7 +139,7 @@ describe("animations", () => {
// insert widget into the DOM // insert widget into the DOM
let def = makeDeferred(); let def = makeDeferred();
var spanNode; var spanNode;
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(spanNode.className).toBe("chimay-enter chimay-enter-active"); expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
cb(); cb();
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to"); expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
@@ -155,7 +155,7 @@ describe("animations", () => {
// remove span from the DOM // remove span from the DOM
def = makeDeferred(); def = makeDeferred();
widget.state.hide = true; widget.state.hide = true;
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(spanNode.className).toBe("chimay-leave chimay-leave-active"); expect(spanNode.className).toBe("chimay-leave chimay-leave-active");
cb(); cb();
expect(spanNode.className).toBe("chimay-leave-active chimay-leave-to"); expect(spanNode.className).toBe("chimay-leave-active chimay-leave-to");
@@ -182,7 +182,7 @@ describe("animations", () => {
// insert widget into the DOM // insert widget into the DOM
let def = makeDeferred(); let def = makeDeferred();
var spanNode; var spanNode;
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(spanNode.className).toBe("chimay-enter chimay-enter-active"); expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
cb(); cb();
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to"); expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
@@ -210,7 +210,7 @@ describe("animations", () => {
let def = makeDeferred(); let def = makeDeferred();
var spanNode; var spanNode;
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>' '<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
); );
@@ -250,7 +250,7 @@ describe("animations", () => {
let def = makeDeferred(); let def = makeDeferred();
var spanNode; var spanNode;
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>' '<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
); );
@@ -275,7 +275,7 @@ describe("animations", () => {
// remove span from the DOM // remove span from the DOM
def = makeDeferred(); def = makeDeferred();
widget.state.display = false; widget.state.display = false;
patchNextFrame(cb => { patchNextFrame((cb) => {
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="__3__">blue</span></div>' '<div><span class="chimay-leave chimay-leave-active" data-owl-key="__3__">blue</span></div>'
); );
@@ -315,7 +315,7 @@ describe("animations", () => {
let def = makeDeferred(); let def = makeDeferred();
let phase = "enter"; let phase = "enter";
patchNextFrame(cb => { patchNextFrame((cb) => {
let spans = fixture.querySelectorAll("span"); let spans = fixture.querySelectorAll("span");
expect(spans.length).toBe(1); expect(spans.length).toBe(1);
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`); expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
@@ -366,7 +366,7 @@ describe("animations", () => {
let def = makeDeferred(); let def = makeDeferred();
let phase = "enter"; let phase = "enter";
patchNextFrame(cb => { patchNextFrame((cb) => {
let spans = fixture.querySelectorAll("span"); let spans = fixture.querySelectorAll("span");
expect(spans.length).toBe(1); expect(spans.length).toBe(1);
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`); expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
@@ -413,36 +413,36 @@ describe("animations", () => {
state = useState({ flag: false }); state = useState({ flag: false });
} }
patchNextFrame(cb => cb()); patchNextFrame((cb) => cb());
const widget = new Parent(); const widget = new Parent();
await widget.mount(fixture); await widget.mount(fixture);
widget.state.flag = true; widget.state.flag = true;
await nextTick(); await nextFrame();
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>'); expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1); expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = false; widget.state.flag = false;
await nextTick(); await nextFrame();
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>' '<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>'
); );
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1); expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = true; widget.state.flag = true;
await nextTick(); await nextFrame();
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__3__">blue</span></div>' '<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__3__">blue</span></div>'
); );
expect(QWeb.utils.transitionInsert).toBeCalledTimes(2); expect(QWeb.utils.transitionInsert).toBeCalledTimes(2);
widget.state.flag = false; widget.state.flag = false;
await nextTick(); await nextFrame();
widget.state.flag = true; widget.state.flag = true;
await nextTick(); await nextFrame();
expect(QWeb.utils.transitionInsert).toBeCalledTimes(3); expect(QWeb.utils.transitionInsert).toBeCalledTimes(3);
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
@@ -534,11 +534,11 @@ exports[`other directives with t-component slot setted value (with t-set) not ac
w3.__owl__.pvnode = pvnode; w3.__owl__.pvnode = pvnode;
} }
w3.__owl__.parentLastFiberId = extra.fiber.id; w3.__owl__.parentLastFiberId = extra.fiber.id;
let c5 = [], p5 = {key:5}; let c6 = [], p6 = {key:6};
let vn5 = h('p', p5, c5); let vn6 = h('p', p6, c6);
c1.push(vn5); c1.push(vn6);
if (scope.iter != null) { if (scope.iter != null) {
c5.push({text: scope.iter}); c6.push({text: scope.iter});
} }
return vn1; return vn1;
}" }"
@@ -1253,12 +1253,16 @@ exports[`other directives with t-component t-set can't alter from within callee
if (scope.iter != null) { if (scope.iter != null) {
c2.push({text: scope.iter}); c2.push({text: scope.iter});
} }
this.subTemplates['ChildWidget'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'})); let _origScope6 = scope;
let c5 = [], p5 = {key:5}; scope = Object.create(scope);
let vn5 = h('p', p5, c5); scope.__access_mode__ = 'ro';
c1.push(vn5); this.constructor.subTemplates['2'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__7__'}));
scope = _origScope6;
let c8 = [], p8 = {key:8};
let vn8 = h('p', p8, c8);
c1.push(vn8);
if (scope.iter != null) { if (scope.iter != null) {
c5.push({text: scope.iter}); c8.push({text: scope.iter});
} }
return vn1; return vn1;
}" }"
@@ -1280,23 +1284,23 @@ exports[`other directives with t-component t-set can't alter in t-call body 1`]
if (scope.iter != null) { if (scope.iter != null) {
c2.push({text: scope.iter}); c2.push({text: scope.iter});
} }
{ let _origScope6 = scope;
let _origScope4 = scope;
scope = Object.create(scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro'; scope.__access_mode__ = 'ro';
{
{ {
let c__0 = []; let c__0 = [];
utils.getScope(scope, 'iter').iter = 'inCall'; utils.getScope(scope, 'iter').iter = 'inCall';
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
this.subTemplates['ChildWidget'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'})); this.constructor.subTemplates['2'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__7__'}));
scope = _origScope4;
} }
let c6 = [], p6 = {key:6}; scope = _origScope6;
let vn6 = h('p', p6, c6); let c8 = [], p8 = {key:8};
c1.push(vn6); let vn8 = h('p', p8, c8);
c1.push(vn8);
if (scope.iter != null) { if (scope.iter != null) {
c6.push({text: scope.iter}); c8.push({text: scope.iter});
} }
return vn1; return vn1;
}" }"
@@ -1401,6 +1405,47 @@ exports[`other directives with t-component t-set outside modified in t-foreach 1
}" }"
`; `;
exports[`props evaluation t-set with a body expression can be used as textual prop 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let c2 = new utils.VDomArray();
c2.push({text: \`42\`});
scope.abc = c2
// Component 'Child'
let w3 = '__4__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__4__']] : false;
let props3 = {val:scope.abc};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined);
let pvnode = w3.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| scope['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap['__4__'] = w3.__owl__.id;
let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__4__', hook: {remove() {},destroy(vn) {w3.destroy();}}});
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
w3.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`random stuff/miscellaneous can inject values in tagged templates 1`] = ` exports[`random stuff/miscellaneous can inject values in tagged templates 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -1410,7 +1455,11 @@ exports[`random stuff/miscellaneous can inject values in tagged templates 1`] =
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
this.subTemplates['__template__1'].call(this, Object.assign(Object.create(context), scope), Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__4__'})); let _origScope5 = 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: '__6__'}));
scope = _origScope5;
return vn1; return vn1;
}" }"
`; `;
@@ -1525,15 +1574,15 @@ exports[`t-call handlers are properly bound through a t-call 1`] = `
// Template name: \\"sub\\" // Template name: \\"sub\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let h = this.h; let h = this.h;
let c1 = extra.parentNode; let c2 = extra.parentNode;
let key0 = extra.key || \\"\\"; let key0 = extra.key || \\"\\";
let c2 = [], p2 = {key:\`\${key0}_2\`,on:{}}; let c3 = [], p3 = {key:\`\${key0}_3\`,on:{}};
let vn2 = h('p', p2, c2); let vn3 = h('p', p3, c3);
c1.push(vn2); c2.push(vn3);
let k3 = \`click__3__\${key0}__\`; let k4 = \`click__4__\${key0}__\`;
extra.handlers[k3] = extra.handlers[k3] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['update'](e);}; extra.handlers[k4] = extra.handlers[k4] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['update'](e);};
p2.on['click'] = extra.handlers[k3]; p3.on['click'] = extra.handlers[k4];
c2.push({text: \`lucas\`}); c3.push({text: \`lucas\`});
}" }"
`; `;
@@ -1544,14 +1593,14 @@ exports[`t-call handlers with arguments are properly bound through a t-call 1`]
let utils = this.constructor.utils; let utils = this.constructor.utils;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c1 = extra.parentNode; let c2 = extra.parentNode;
let key0 = extra.key || \\"\\"; let key0 = extra.key || \\"\\";
let c2 = [], p2 = {key:\`\${key0}_2\`,on:{}}; let c3 = [], p3 = {key:\`\${key0}_3\`,on:{}};
let vn2 = h('p', p2, c2); let vn3 = h('p', p3, c3);
c1.push(vn2); c2.push(vn3);
let args3 = [scope['a']]; let args4 = [scope['a']];
p2.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['update'](...args3, e);}; p3.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['update'](...args4, e);};
c2.push({text: \`lucas\`}); c3.push({text: \`lucas\`});
}" }"
`; `;
@@ -1564,32 +1613,32 @@ exports[`t-call parent is set within t-call 1`] = `
let parent = extra.parent; let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c1 = extra.parentNode; let c2 = extra.parentNode;
let key0 = extra.key || \\"\\"; let key0 = extra.key || \\"\\";
// Component 'Child' // Component 'Child'
let k3 = \`__3__\${key0}__\`; let k4 = \`__4__\${key0}__\`;
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false; let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let props2 = {}; let props3 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) { if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w2.destroy(); w3.destroy();
w2 = false; w3 = false;
} }
if (w2) { if (w3) {
w2.__updateProps(props2, extra.fiber, undefined); w3.__updateProps(props3, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode; let pvnode = w3.__owl__.pvnode;
c1.push(pvnode); c2.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey3 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child']; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w2 = new W2(parent, props2); w3 = new W3(parent, props3);
parent.__owl__.cmap[k3] = w2.__owl__.id; parent.__owl__.cmap[k4] = w3.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {w3.destroy();}}});
c1.push(pvnode); c2.push(pvnode);
w2.__owl__.pvnode = pvnode; w3.__owl__.pvnode = pvnode;
} }
w2.__owl__.parentLastFiberId = extra.fiber.id; w3.__owl__.parentLastFiberId = extra.fiber.id;
}" }"
`; `;
@@ -1683,6 +1732,33 @@ exports[`t-model directive basic use, on an input 1`] = `
}" }"
`; `;
exports[`t-model directive basic use, on an input with bracket expression 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('input', p2, c2);
c1.push(vn2);
let expr2 = scope['state'];
let exprKey2 = 'text';
p2.props = {value: expr2[exprKey2]};
extra.handlers['__3__'] = extra.handlers['__3__'] || ((ev) => {expr2[exprKey2] = ev.target.value});
p2.on['input'] = extra.handlers['__3__'];
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
let _5 = scope['state'].text;
if (_5 != null) {
c4.push({text: _5});
}
return vn1;
}"
`;
exports[`t-model directive basic use, on another key in component 1`] = ` exports[`t-model directive basic use, on another key in component 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -1749,6 +1825,46 @@ exports[`t-model directive in a t-foreach 1`] = `
}" }"
`; `;
exports[`t-model directive in a t-foreach, part 2 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let _2 = scope['state'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
}
let _length3 = _3.length;
let _origScope5 = scope;
scope = Object.create(scope);
for (let i1 = 0; i1 < _length3; i1++) {
scope.thing_first = i1 === 0
scope.thing_last = i1 === _length3 - 1
scope.thing_index = i1
scope.thing = _3[i1]
scope.thing_value = _4[i1]
let key1 = scope['thing_index'];
let c6 = [], p6 = {key:\`\${key1}_6\`,on:{}};
let vn6 = h('input', p6, c6);
c1.push(vn6);
let expr6 = scope['state'];
let exprKey6 = scope['thing_index'];
let k7 = \`__7__\${key1}__\`;
p6.props = {value: expr6[exprKey6]};
extra.handlers[k7] = extra.handlers[k7] || ((ev) => {expr6[exprKey6] = ev.target.value});
p6.on['input'] = extra.handlers[k7];
}
scope = _origScope5;
return vn1;
}"
`;
exports[`t-model directive on a select 1`] = ` exports[`t-model directive on a select 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -1833,7 +1949,7 @@ exports[`t-model directive on an input type=radio 1`] = `
let _2 = 'radio'; let _2 = 'radio';
let _3 = 'one'; let _3 = 'one';
let _4 = 'One'; let _4 = 'One';
let c5 = [], p5 = {key:5,attrs:{type: _2,id: _3,value: _4},on:{}}; let c5 = [], p5 = {key:5,attrs:{type: _2,id: _3,value: _4},props:{value: _4},on:{}};
let vn5 = h('input', p5, c5); let vn5 = h('input', p5, c5);
c1.push(vn5); c1.push(vn5);
let expr5 = scope['state']; let expr5 = scope['state'];
@@ -1843,7 +1959,7 @@ exports[`t-model directive on an input type=radio 1`] = `
let _7 = 'radio'; let _7 = 'radio';
let _8 = 'two'; let _8 = 'two';
let _9 = 'Two'; let _9 = 'Two';
let c10 = [], p10 = {key:10,attrs:{type: _7,id: _8,value: _9},on:{}}; let c10 = [], p10 = {key:10,attrs:{type: _7,id: _8,value: _9},props:{value: _9},on:{}};
let vn10 = h('input', p10, c10); let vn10 = h('input', p10, c10);
c1.push(vn10); c1.push(vn10);
let expr10 = scope['state']; let expr10 = scope['state'];
+144 -123
View File
@@ -44,23 +44,23 @@ exports[`t-slot directive can define and call slots 2`] = `
) { ) {
// Template name: \\"Dialog\\" // Template name: \\"Dialog\\"
let h = this.h; 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 c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9); let vn9 = h('div', p9, c9);
c6.push(vn9); let c10 = [], p10 = {key:10};
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer']; let vn10 = h('div', p10, c10);
if (slot10) { c9.push(vn10);
slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context})); const slot11 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot11) {
slot11.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c10, parent: extra.parent || context}));
} }
return vn6; let c12 = [], p12 = {key:12};
let vn12 = h('div', p12, c12);
c9.push(vn12);
const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot13) {
slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
}
return vn9;
}" }"
`; `;
@@ -69,11 +69,11 @@ exports[`t-slot directive can define and call slots 3`] = `
) { ) {
// Template name: \\"slot_header_template\\" // Template name: \\"slot_header_template\\"
let h = this.h; let h = this.h;
let c1 = extra.parentNode; let c4 = extra.parentNode;
let c4 = [], p4 = {key:4}; let c5 = [], p5 = {key:5};
let vn4 = h('span', p4, c4); let vn5 = h('span', p5, c5);
c1.push(vn4); c4.push(vn5);
c4.push({text: \`header\`}); c5.push({text: \`header\`});
}" }"
`; `;
@@ -82,11 +82,11 @@ exports[`t-slot directive can define and call slots 4`] = `
) { ) {
// Template name: \\"slot_footer_template\\" // Template name: \\"slot_footer_template\\"
let h = this.h; let h = this.h;
let c1 = extra.parentNode; let c6 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c7 = [], p7 = {key:7};
let vn5 = h('span', p5, c5); let vn7 = h('span', p7, c7);
c1.push(vn5); c6.push(vn7);
c5.push({text: \`footer\`}); c7.push({text: \`footer\`});
}" }"
`; `;
@@ -134,23 +134,23 @@ exports[`t-slot directive can define and call slots using old t-set keyword 2`]
) { ) {
// Template name: \\"__template__1\\" // Template name: \\"__template__1\\"
let h = this.h; 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 c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9); let vn9 = h('div', p9, c9);
c6.push(vn9); let c10 = [], p10 = {key:10};
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer']; let vn10 = h('div', p10, c10);
if (slot10) { c9.push(vn10);
slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context})); const slot11 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot11) {
slot11.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c10, parent: extra.parent || context}));
} }
return vn6; let c12 = [], p12 = {key:12};
let vn12 = h('div', p12, c12);
c9.push(vn12);
const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot13) {
slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
}
return vn9;
}" }"
`; `;
@@ -159,11 +159,11 @@ exports[`t-slot directive can define and call slots using old t-set keyword 3`]
) { ) {
// Template name: \\"slot_header_template\\" // Template name: \\"slot_header_template\\"
let h = this.h; let h = this.h;
let c1 = extra.parentNode; let c4 = extra.parentNode;
let c4 = [], p4 = {key:4}; let c5 = [], p5 = {key:5};
let vn4 = h('span', p4, c4); let vn5 = h('span', p5, c5);
c1.push(vn4); c4.push(vn5);
c4.push({text: \`header\`}); c5.push({text: \`header\`});
}" }"
`; `;
@@ -172,11 +172,11 @@ exports[`t-slot directive can define and call slots using old t-set keyword 4`]
) { ) {
// Template name: \\"slot_footer_template\\" // Template name: \\"slot_footer_template\\"
let h = this.h; let h = this.h;
let c1 = extra.parentNode; let c6 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c7 = [], p7 = {key:7};
let vn5 = h('span', p5, c5); let vn7 = h('span', p7, c7);
c1.push(vn5); c6.push(vn7);
c5.push({text: \`footer\`}); c7.push({text: \`footer\`});
}" }"
`; `;
@@ -185,11 +185,11 @@ exports[`t-slot directive content is the default slot 1`] = `
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let h = this.h; let h = this.h;
let c1 = extra.parentNode; let c4 = extra.parentNode;
let c4 = [], p4 = {key:4}; let c5 = [], p5 = {key:5};
let vn4 = h('span', p4, c4); let vn5 = h('span', p5, c5);
c1.push(vn4); c4.push(vn5);
c4.push({text: \`sts rocks\`}); c5.push({text: \`sts rocks\`});
}" }"
`; `;
@@ -215,8 +215,27 @@ exports[`t-slot directive default slot work with text nodes 1`] = `
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let h = this.h; let h = this.h;
let c1 = extra.parentNode; let c4 = extra.parentNode;
c1.push({text: \`sts rocks\`}); c4.push({text: \`sts rocks\`});
}"
`;
exports[`t-slot directive dynamic t-slot call 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c10 = [], p10 = {key:10,on:{}};
let vn10 = h('button', p10, c10);
extra.handlers['click__11__'] = extra.handlers['click__11__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['toggle'](e);};
p10.on['click'] = extra.handlers['click__11__'];
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + (scope['current'].slot)];
if (slot12) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c10, parent: extra.parent || context}));
}
return vn10;
}" }"
`; `;
@@ -225,15 +244,15 @@ exports[`t-slot directive multiple roots are allowed in a default slot 1`] = `
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let h = this.h; let h = this.h;
let c1 = extra.parentNode; let c4 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts\`});
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5); let vn5 = h('span', p5, c5);
c1.push(vn5); c4.push(vn5);
c5.push({text: \`rocks\`}); c5.push({text: \`sts\`});
let c6 = [], p6 = {key:6};
let vn6 = h('span', p6, c6);
c4.push(vn6);
c6.push({text: \`rocks\`});
}" }"
`; `;
@@ -242,15 +261,15 @@ exports[`t-slot directive multiple roots are allowed in a named slot 1`] = `
) { ) {
// Template name: \\"slot_content_template\\" // Template name: \\"slot_content_template\\"
let h = this.h; let h = this.h;
let c1 = extra.parentNode; let c4 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts\`});
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5); let vn5 = h('span', p5, c5);
c1.push(vn5); c4.push(vn5);
c5.push({text: \`rocks\`}); c5.push({text: \`sts\`});
let c6 = [], p6 = {key:6};
let vn6 = h('span', p6, c6);
c4.push(vn6);
c6.push({text: \`rocks\`});
}" }"
`; `;
@@ -278,22 +297,22 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
let utils = this.constructor.utils; let utils = this.constructor.utils;
context.__owl__.refs = context.__owl__.refs || {}; context.__owl__.refs = context.__owl__.refs || {};
let h = this.h; let h = this.h;
let c1 = extra.parentNode; let c8 = extra.parentNode;
let c8 = [], p8 = {key:8,on:{}}; let c9 = [], p9 = {key:9,on:{}};
let vn8 = h('button', p8, c8); let vn9 = h('button', p9, c9);
c1.push(vn8); c8.push(vn9);
extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);}; extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p8.on['click'] = extra.handlers['click__9__']; p9.on['click'] = extra.handlers['click__10__'];
const ref10 = \`myButton\`; const ref11 = \`myButton\`;
p8.hook = { p9.hook = {
create: (_, n) => { create: (_, n) => {
context.__owl__.refs[ref10] = n.elm; context.__owl__.refs[ref11] = n.elm;
}, },
destroy: () => { destroy: () => {
delete context.__owl__.refs[ref10]; delete context.__owl__.refs[ref11];
}, },
}; };
c8.push({text: \`do something\`}); c9.push({text: \`do something\`});
}" }"
`; `;
@@ -303,13 +322,13 @@ exports[`t-slot directive slots are rendered with proper context 1`] = `
// Template name: \\"slot_footer_template\\" // Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let h = this.h; let h = this.h;
let c1 = extra.parentNode; let c8 = extra.parentNode;
let c8 = [], p8 = {key:8,on:{}}; let c9 = [], p9 = {key:9,on:{}};
let vn8 = h('button', p8, c8); let vn9 = h('button', p9, c9);
c1.push(vn8); c8.push(vn9);
extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);}; extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p8.on['click'] = extra.handlers['click__9__']; p9.on['click'] = extra.handlers['click__10__'];
c8.push({text: \`do something\`}); c9.push({text: \`do something\`});
}" }"
`; `;
@@ -319,14 +338,14 @@ exports[`t-slot directive slots are rendered with proper context, part 2 1`] = `
// Template name: \\"Link\\" // Template name: \\"Link\\"
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let _11 = scope['props'].to; let _12 = scope['props'].to;
let c12 = [], p12 = {key:12,attrs:{href: _11}}; let c13 = [], p13 = {key:13,attrs:{href: _12}};
let vn12 = h('a', p12, c12); let vn13 = h('a', p13, c13);
const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot14 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot13) { if (slot14) {
slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context})); slot14.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c13, parent: extra.parent || context}));
} }
return vn12; return vn13;
}" }"
`; `;
@@ -401,11 +420,11 @@ exports[`t-slot directive slots are rendered with proper context, part 2 3`] = `
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c7 = extra.parentNode; let c10 = extra.parentNode;
c7.push({text: \`User \`}); c10.push({text: \`User \`});
let _10 = scope['user'].name; let _11 = scope['user'].name;
if (_10 != null) { if (_11 != null) {
c7.push({text: _10}); c10.push({text: _11});
} }
}" }"
`; `;
@@ -416,14 +435,14 @@ exports[`t-slot directive slots are rendered with proper context, part 3 1`] = `
// Template name: \\"Link\\" // Template name: \\"Link\\"
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let _10 = scope['props'].to; let _12 = scope['props'].to;
let c11 = [], p11 = {key:11,attrs:{href: _10}}; let c13 = [], p13 = {key:13,attrs:{href: _12}};
let vn11 = h('a', p11, c11); let vn13 = h('a', p13, c13);
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot14 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot12) { if (slot14) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c11, parent: extra.parent || context})); slot14.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c13, parent: extra.parent || context}));
} }
return vn11; return vn13;
}" }"
`; `;
@@ -499,9 +518,10 @@ exports[`t-slot directive slots are rendered with proper context, part 3 3`] = `
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c7 = extra.parentNode; let c10 = extra.parentNode;
if (scope.userdescr != null) { let _11 = scope['userdescr'];
c7.push({text: scope.userdescr}); if (_11 != null) {
c10.push({text: _11});
} }
}" }"
`; `;
@@ -552,9 +572,10 @@ exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c1 = extra.parentNode; let c4 = extra.parentNode;
if (scope.userdescr != null) { let _5 = scope['userdescr'];
c1.push({text: scope.userdescr}); if (_5 != null) {
c4.push({text: _5});
} }
}" }"
`; `;
@@ -564,13 +585,13 @@ exports[`t-slot directive t-set t-value in a slot 1`] = `
) { ) {
// Template name: \\"__template__1\\" // Template name: \\"__template__1\\"
let h = this.h; let h = this.h;
let c4 = [], p4 = {key:4}; let c5 = [], p5 = {key:5};
let vn4 = h('span', p4, c4); let vn5 = h('span', p5, c5);
const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot6 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot5) { if (slot6) {
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context})); slot6.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c5, parent: extra.parent || context}));
} }
return vn4; return vn5;
}" }"
`; `;
@@ -581,12 +602,12 @@ exports[`t-slot directive template can just return a slot 1`] = `
let utils = this.constructor.utils; let utils = this.constructor.utils;
let result; let result;
let h = this.h; let h = this.h;
const slot6 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot7 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot6) { if (slot7) {
let children7= [] let children8= []
result = {} result = {}
slot6.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: children7, parent: extra.parent || context})); slot7.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: children8, parent: extra.parent || context}));
utils.defineProxy(result, children7[0]); utils.defineProxy(result, children8[0]);
} }
return result; return result;
}" }"
+2 -2
View File
@@ -27,7 +27,7 @@ afterEach(() => {
function children(w: Component): Component[] { function children(w: Component): Component[] {
const childrenMap = w.__owl__.children; const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]); return Object.keys(childrenMap).map((id) => childrenMap[id]);
} }
describe("async rendering", () => { describe("async rendering", () => {
@@ -404,7 +404,7 @@ describe("async rendering", () => {
} }
class ChildB extends Component { class ChildB extends Component {
willStart(): any { willStart(): any {
return new Promise(function() {}); return new Promise(function () {});
} }
} }
class Parent extends Component { class Parent extends Component {
+242 -52
View File
@@ -1,16 +1,16 @@
import { Component, Env } from "../../src/component/component"; import { Component, Env, mount } from "../../src/component/component";
import { EventBus } from "../../src/core/event_bus";
import { useRef, useState } from "../../src/hooks";
import { QWeb } from "../../src/qweb/qweb"; import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
import { useState, useRef } from "../../src/hooks";
import { EventBus } from "../../src/core/event_bus";
import { import {
editInput,
makeDeferred, makeDeferred,
makeTestFixture,
makeTestEnv, makeTestEnv,
makeTestFixture,
nextMicroTick, nextMicroTick,
nextTick, nextTick,
normalize, normalize,
editInput
} from "../helpers"; } from "../helpers";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -39,7 +39,7 @@ afterEach(() => {
function children(w: Component): Component[] { function children(w: Component): Component[] {
const childrenMap = w.__owl__.children; const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]); return Object.keys(childrenMap).map((id) => childrenMap[id]);
} }
// Test components // Test components
@@ -69,18 +69,23 @@ describe("basic widget properties", () => {
class SomeWidget extends Component { class SomeWidget extends Component {
static template = xml`<div>content</div>`; static template = xml`<div>content</div>`;
} }
const widget = new SomeWidget(); await mount(SomeWidget, { target: fixture });
widget.mount(fixture);
await nextTick();
expect(fixture.innerHTML).toBe("<div>content</div>"); expect(fixture.innerHTML).toBe("<div>content</div>");
}); });
test("can be mounted with props", async () => {
class SomeWidget extends Component {
static template = xml`<div><t t-esc="props.content"/></div>`;
}
await mount(SomeWidget, { target: fixture, props: { content: "foo" } });
expect(fixture.innerHTML).toBe("<div>foo</div>");
});
test("can be mounted on a documentFragment", async () => { test("can be mounted on a documentFragment", async () => {
class SomeWidget extends Component { class SomeWidget extends Component {
static template = xml`<div>content</div>`; static template = xml`<div>content</div>`;
} }
const widget = new SomeWidget(); const widget = await mount(SomeWidget, { target: document.createDocumentFragment() });
await widget.mount(document.createDocumentFragment());
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
await widget.mount(fixture); await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>content</div>"); expect(fixture.innerHTML).toBe("<div>content</div>");
@@ -90,10 +95,9 @@ describe("basic widget properties", () => {
class SomeWidget extends Component { class SomeWidget extends Component {
static template = xml`<div>content</div>`; static template = xml`<div>content</div>`;
} }
const widget = new SomeWidget();
let error; let error;
try { try {
await widget.mount(null as any); await mount(SomeWidget, { target: null as any });
} catch (e) { } catch (e) {
error = e; error = e;
} }
@@ -107,10 +111,9 @@ describe("basic widget properties", () => {
class SomeWidget extends Component { class SomeWidget extends Component {
static template = xml`<t/>`; static template = xml`<t/>`;
} }
const widget = new SomeWidget();
let error; let error;
try { try {
await widget.mount(fixture); await mount(SomeWidget, { target: fixture });
} catch (e) { } catch (e) {
error = e; error = e;
} }
@@ -133,12 +136,11 @@ describe("basic widget properties", () => {
static template = xml` static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`; <div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
state = useState({ state = useState({
counter: 0 counter: 0,
}); });
} }
const counter = new Counter(); const counter = await mount(Counter, { target: fixture });
counter.mount(fixture);
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div>0<button>Inc</button></div>"); expect(fixture.innerHTML).toBe("<div>0<button>Inc</button></div>");
const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0]; const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0];
@@ -156,8 +158,7 @@ describe("basic widget properties", () => {
static components = { Child }; static components = { Child };
} }
const parent = new Parent(); await mount(Parent, { target: fixture });
await parent.mount(fixture);
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe("<div><span></span></div>"); expect(fixture.innerHTML).toBe("<div><span></span></div>");
}); });
@@ -167,13 +168,12 @@ describe("basic widget properties", () => {
static template = xml` static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`; <div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
state = useState({ state = useState({
counter: 0 counter: 0,
}); });
} }
const counter = new Counter();
const target = document.createElement("div"); const target = document.createElement("div");
await counter.mount(target); const counter = await mount(Counter, { target: target });
expect(target.innerHTML).toBe("<div>0<button>Inc</button></div>"); expect(target.innerHTML).toBe("<div>0<button>Inc</button></div>");
const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0]; const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0];
button.click(); button.click();
@@ -188,8 +188,7 @@ describe("basic widget properties", () => {
<div style="font-weight:bold;" class="some-class">world</div> <div style="font-weight:bold;" class="some-class">world</div>
`; `;
} }
const widget = new StyledWidget(); await mount(StyledWidget, { target: fixture });
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div style="font-weight:bold;" class="some-class">world</div>`); expect(fixture.innerHTML).toBe(`<div style="font-weight:bold;" class="some-class">world</div>`);
}); });
@@ -212,8 +211,8 @@ describe("basic widget properties", () => {
steps.push("patched"); steps.push("patched");
} }
} }
const widget = new TestW(); await mount(TestW, { target: fixture });
await widget.mount(fixture);
expect(steps).toEqual(["__render", "mounted"]); expect(steps).toEqual(["__render", "mounted"]);
}); });
@@ -537,7 +536,7 @@ describe("lifecycle hooks", () => {
"child:mounted", "child:mounted",
"parent:willUnmount", "parent:willUnmount",
"child:willUnmount", "child:willUnmount",
"childchild:willUnmount" "childchild:willUnmount",
]); ]);
}); });
@@ -589,7 +588,7 @@ describe("lifecycle hooks", () => {
"childchild:willPatch", "childchild:willPatch",
"childchild:patched", "childchild:patched",
"child:patched", "child:patched",
"parent:patched" "parent:patched",
]); ]);
}); });
@@ -782,7 +781,7 @@ describe("lifecycle hooks", () => {
"c mounted", "c mounted",
"p mounted", "p mounted",
"p willunmount", "p willunmount",
"c willunmount" "c willunmount",
]); ]);
}); });
@@ -960,7 +959,7 @@ describe("lifecycle hooks", () => {
"parent:willPatch", "parent:willPatch",
"child:willPatch", "child:willPatch",
"child:patched", "child:patched",
"parent:patched" "parent:patched",
]); ]);
}); });
}); });
@@ -1134,7 +1133,7 @@ describe("composition", () => {
class App extends Component { class App extends Component {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`; static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = useState({ state = useState({
child: "a" child: "a",
}); });
get myComponent() { get myComponent() {
return this.state.child === "a" ? A : B; return this.state.child === "a" ? A : B;
@@ -1158,7 +1157,7 @@ describe("composition", () => {
class App extends Component { class App extends Component {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`; static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = useState({ state = useState({
child: "a" child: "a",
}); });
get myComponent() { get myComponent() {
return this.state.child === "a" ? A : B; return this.state.child === "a" ? A : B;
@@ -1228,7 +1227,7 @@ describe("composition", () => {
static template = xml` static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`; <div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
state = useState({ state = useState({
counter: 0 counter: 0,
}); });
} }
@@ -1327,7 +1326,7 @@ describe("composition", () => {
static template = xml` static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`; <div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
state = useState({ state = useState({
counter: 0 counter: 0,
}); });
} }
@@ -1350,7 +1349,7 @@ describe("composition", () => {
static template = xml` static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`; <div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
state = useState({ state = useState({
counter: 0 counter: 0,
}); });
} }
@@ -1389,7 +1388,7 @@ describe("composition", () => {
); );
class Parent extends Component { class Parent extends Component {
state = useState({ state = useState({
numbers: [1, 2, 3] numbers: [1, 2, 3],
}); });
static components = { ChildWidget }; static components = { ChildWidget };
} }
@@ -1429,7 +1428,7 @@ describe("composition", () => {
class Parent extends Component { class Parent extends Component {
static template = "parent"; static template = "parent";
state = useState({ state = useState({
numbers: [1, 2, 3] numbers: [1, 2, 3],
}); });
static components = { ChildWidget }; static components = { ChildWidget };
} }
@@ -1513,8 +1512,8 @@ describe("composition", () => {
blips: [ blips: [
{ a: "a", id: 1 }, { a: "a", id: 1 },
{ b: "b", id: 2 }, { b: "b", id: 2 },
{ c: "c", id: 4 } { c: "c", id: 4 },
] ],
}); });
} }
const parent = new Parent(); const parent = new Parent();
@@ -1621,8 +1620,8 @@ describe("composition", () => {
records: [ records: [
{ id: 1, val: 1 }, { id: 1, val: 1 },
{ id: 2, val: 2 }, { id: 2, val: 2 },
{ id: 3, val: 3 } { id: 3, val: 3 },
] ],
}); });
static components = { ChildWidget }; static components = { ChildWidget };
} }
@@ -1725,6 +1724,47 @@ describe("props evaluation ", () => {
await widget.mount(fixture); await widget.mount(fixture);
expect(normalize(fixture.innerHTML)).toBe("<div><span>42</span></div>"); expect(normalize(fixture.innerHTML)).toBe("<div><span>42</span></div>");
}); });
test("t-set with a body expression can be used as textual prop", async () => {
class Child extends Component {
static template = xml`<span t-esc="props.val"/>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
<t t-set="abc">42</t>
<Child val="abc"/>
</div>`;
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
});
test("t-set with a body expression can be passed in props, and then t-raw", async () => {
class Child extends Component {
static template = xml`
<span>
<t t-esc="props.val"/>
<t t-raw="props.val"/>
</span>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
<t t-set="abc"><p>43</p></t>
<Child val="abc"/>
</div>`;
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>&lt;p&gt;4343&lt;/p&gt;<p>43</p></span></div>");
});
}); });
describe("other directives with t-component", () => { describe("other directives with t-component", () => {
@@ -2003,7 +2043,7 @@ describe("other directives with t-component", () => {
} }
const widget = new ParentWidget(); const widget = new ParentWidget();
await widget.mount(fixture); await widget.mount(fixture);
(<HTMLElement>fixture).addEventListener("ev", function(e) { (<HTMLElement>fixture).addEventListener("ev", function (e) {
steps.push(e.defaultPrevented); steps.push(e.defaultPrevented);
}); });
@@ -2038,7 +2078,7 @@ describe("other directives with t-component", () => {
} }
const widget = new ParentWidget(); const widget = new ParentWidget();
await widget.mount(fixture); await widget.mount(fixture);
(<HTMLElement>fixture).addEventListener("ev", function(e) { (<HTMLElement>fixture).addEventListener("ev", function (e) {
steps.push(e.defaultPrevented); steps.push(e.defaultPrevented);
}); });
@@ -2976,7 +3016,7 @@ describe("random stuff/miscellaneous", () => {
"D:mounted", "D:mounted",
"C:mounted", "C:mounted",
"B:mounted", "B:mounted",
"A:mounted" "A:mounted",
]); ]);
// update // update
@@ -2999,7 +3039,7 @@ describe("random stuff/miscellaneous", () => {
"E:destroy", "E:destroy",
"F:mounted", "F:mounted",
"D:patched", "D:patched",
"C:patched" "C:patched",
]); ]);
}); });
@@ -3015,6 +3055,85 @@ describe("random stuff/miscellaneous", () => {
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe("<div><span>42</span></div>"); expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
}); });
test("update props of component without concrete own node", async () => {
class Custom extends Component {
static template = xml`
<div class="widget-subkey">
<t t-esc="props.key"/>__<t t-esc="props.subKey"/>
</div>`;
}
class Child extends Component {
static components = { Custom };
static template = xml`
<t t-component="Custom"
t-key="props.subKey"
key="props.key"
subKey="props.subKey"/>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
<Child t-key="childProps.key" t-props="childProps"/>
</div>`;
childProps = {
key: 1,
subKey: 1,
};
}
const parent = new Parent(null);
await parent.mount(fixture);
expect(fixture.textContent!.trim()).toBe("1__1");
// First step: change the Custom's instance
Object.assign(parent.childProps, {
subKey: 2,
});
parent.render();
await nextTick();
expect(fixture.textContent!.trim()).toBe("1__2");
// Second step, change both Child's and Custom's instance
Object.assign(parent.childProps, {
key: 2,
subKey: 3,
});
parent.render();
await nextTick();
expect(fixture.textContent!.trim()).toBe("2__3");
});
test("two renderings initiated between willPatch and patched", async () => {
let app;
class Panel extends Component {
static template = xml`<abc><t t-esc="props.val"/></abc>`;
mounted() {
app.render();
}
willUnmount() {
app.render();
}
}
// Main root component
class App extends Component {
static components = { Panel };
static template = xml`<div><Panel t-key="'panel_' + state.panel" val="state.panel"/></div>`;
state = useState({ panel: "Panel1" });
}
app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><abc>Panel1</abc></div>");
app.state.panel = "Panel2";
await nextTick();
expect(fixture.innerHTML).toBe("<div><abc>Panel2</abc></div>");
});
}); });
describe("widget and observable state", () => { describe("widget and observable state", () => {
@@ -3138,6 +3257,46 @@ describe("t-model directive", () => {
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
}); });
test("basic use, on an input with bracket expression", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state['text']"/>
<span><t t-esc="state.text"/></span>
</div>`;
state = useState({ text: "" });
}
const comp = new SomeComponent();
await comp.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
const input = fixture.querySelector("input")!;
await editInput(input, "test");
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
});
test("throws if invalid expression", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state"/>
</div>`;
state = useState({ text: "" });
}
const comp = new SomeComponent();
let error;
try {
await comp.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Invalid t-model expression: "state" (it should be assignable)`);
});
test("basic use, on another key in component", async () => { test("basic use, on another key in component", async () => {
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
@@ -3410,7 +3569,7 @@ describe("t-model directive", () => {
state = useState([ state = useState([
{ f: false, id: 1 }, { f: false, id: 1 },
{ f: false, id: 2 }, { f: false, id: 2 },
{ f: false, id: 3 } { f: false, id: 3 },
]); ]);
} }
const comp = new SomeComponent(); const comp = new SomeComponent();
@@ -3427,6 +3586,28 @@ describe("t-model directive", () => {
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
}); });
test("in a t-foreach, part 2", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<t t-foreach="state" t-as="thing" t-key="thing_index" >
<input t-model="state[thing_index]"/>
</t>
</div>
`;
state = useState(["zuko", "iroh"]);
}
const comp = new SomeComponent();
await comp.mount(fixture);
expect(comp.state).toEqual(["zuko", "iroh"]);
const input = fixture.querySelectorAll("input")[1]!;
input.value = "uncle iroh";
input.dispatchEvent(new Event("input"));
expect(comp.state).toEqual(["zuko", "uncle iroh"]);
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
});
test("two inputs in a div with a t-key", async () => { test("two inputs in a div with a t-key", async () => {
class SomeComponent extends Component { class SomeComponent extends Component {
static template = xml` static template = xml`
@@ -3455,7 +3636,7 @@ describe("environment and plugins", () => {
let bus = new EventBus(); let bus = new EventBus();
// definition of a plugin // definition of a plugin
const somePlugin = env => { const somePlugin = (env) => {
env.someFlag = true; env.someFlag = true;
bus.on("some-event", null, () => { bus.on("some-event", null, () => {
env.someFlag = !env.someFlag; env.someFlag = !env.someFlag;
@@ -3825,7 +4006,9 @@ describe("t-call", () => {
expect(fixture.innerHTML).toBe("<div><p>lucas</p></div>"); expect(fixture.innerHTML).toBe("<div><p>lucas</p></div>");
fixture.querySelector("p")!.click(); fixture.querySelector("p")!.click();
expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot(); const subId = env.qweb.subTemplates["sub"];
const subFn = QWeb.subTemplates[subId] as any;
expect(subFn.toString()).toMatchSnapshot();
}); });
test("parent is set within t-call", async () => { test("parent is set within t-call", async () => {
@@ -3847,7 +4030,10 @@ describe("t-call", () => {
expect(fixture.innerHTML).toBe("<div><span>lucas</span></div>"); expect(fixture.innerHTML).toBe("<div><span>lucas</span></div>");
expect(child.__owl__.parent).toBe(parent); expect(child.__owl__.parent).toBe(parent);
expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot(); const subId = env.qweb.subTemplates["sub"];
const subFn = QWeb.subTemplates[subId] as any;
expect(subFn.toString()).toMatchSnapshot();
}); });
test("t-call in t-foreach and children component", async () => { test("t-call in t-foreach and children component", async () => {
@@ -3889,7 +4075,9 @@ describe("t-call", () => {
expect(fixture.innerHTML).toBe("<span>lucas</span>"); expect(fixture.innerHTML).toBe("<span>lucas</span>");
expect(child.__owl__.parent).toBe(parent); expect(child.__owl__.parent).toBe(parent);
expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot(); const subId = env.qweb.subTemplates["sub"];
const subFn = QWeb.subTemplates[subId] as any;
expect(subFn.toString()).toMatchSnapshot();
}); });
test("handlers with arguments are properly bound through a t-call", async () => { test("handlers with arguments are properly bound through a t-call", async () => {
@@ -3905,7 +4093,9 @@ describe("t-call", () => {
} }
const parent = new Parent(); const parent = new Parent();
await parent.mount(fixture); await parent.mount(fixture);
expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot(); const subId = env.qweb.subTemplates["sub"];
const subFn = QWeb.subTemplates[subId] as any;
expect(subFn.toString()).toMatchSnapshot();
fixture.querySelector("p")!.click(); fixture.querySelector("p")!.click();
}); });
+40 -15
View File
@@ -92,7 +92,7 @@ describe("props validation", () => {
{ type: String, ok: "1", ko: 1 }, { type: String, ok: "1", ko: 1 },
{ type: Object, ok: {}, ko: "1" }, { type: Object, ok: {}, ko: "1" },
{ type: Date, ok: new Date(), ko: "1" }, { type: Date, ok: new Date(), ko: "1" },
{ type: Function, ok: () => {}, ko: "1" } { type: Function, ok: () => {}, ko: "1" },
]; ];
let props; let props;
@@ -149,7 +149,7 @@ describe("props validation", () => {
{ type: String, ok: "1", ko: 1 }, { type: String, ok: "1", ko: 1 },
{ type: Object, ok: {}, ko: "1" }, { type: Object, ok: {}, ko: "1" },
{ type: Date, ok: new Date(), ko: "1" }, { type: Date, ok: new Date(), ko: "1" },
{ type: Function, ok: () => {}, ko: "1" } { type: Function, ok: () => {}, ko: "1" },
]; ];
let props; let props;
@@ -396,7 +396,7 @@ describe("props validation", () => {
class TestWidget extends Component { class TestWidget extends Component {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
static props = { static props = {
p: { type: Object, shape: { id: Number, url: String } } p: { type: Object, shape: { id: Number, url: String } },
}; };
} }
class Parent extends Component { class Parent extends Component {
@@ -458,9 +458,9 @@ describe("props validation", () => {
type: Object, type: Object,
shape: { shape: {
id: Number, id: Number,
url: [Boolean, { type: Array, element: Number }] url: [Boolean, { type: Array, element: Number }],
} },
} },
}; };
} }
class Parent extends Component { class Parent extends Component {
@@ -510,10 +510,10 @@ describe("props validation", () => {
element: { element: {
type: Object, type: Object,
shape: { shape: {
num: { type: Number, optional: true } num: { type: Number, optional: true },
} },
} },
} },
}; };
} }
let error; let error;
@@ -539,8 +539,8 @@ describe("props validation", () => {
class TestComponent extends Component { class TestComponent extends Component {
static props = { static props = {
size: { size: {
validate: e => ["small", "medium", "large"].includes(e) validate: (e) => ["small", "medium", "large"].includes(e),
} },
}; };
} }
let error; let error;
@@ -561,13 +561,13 @@ describe("props validation", () => {
}); });
test("can validate with a custom validator, and a type", () => { test("can validate with a custom validator, and a type", () => {
const validator = jest.fn(n => 0 <= n && n <= 10); const validator = jest.fn((n) => 0 <= n && n <= 10);
class TestComponent extends Component { class TestComponent extends Component {
static props = { static props = {
n: { n: {
type: Number, type: Number,
validate: validator validate: validator,
} },
}; };
} }
let error; let error;
@@ -786,6 +786,31 @@ describe("props validation", () => {
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div><div>4</div></div>"); expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
}); });
test("mix of optional and mandatory", async () => {
class Child extends Component {
static props = {
optional: { type: String, optional: true },
mandatory: Number,
};
static template = xml` <div><t t-esc="props.mandatory"/></div>`;
}
class App extends Component {
static components = { Child };
static template = xml`<div><Child/></div>`;
}
const w = new App(undefined, {});
let error;
try {
await w.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Missing props 'mandatory' (component 'Child')");
});
}); });
describe("default props", () => { describe("default props", () => {
+195 -6
View File
@@ -1,4 +1,4 @@
import { Component, Env } from "../../src/component/component"; import { Component, Env, mount } from "../../src/component/component";
import { QWeb } from "../../src/qweb/qweb"; import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
import { useState, useRef } from "../../src/hooks"; import { useState, useRef } from "../../src/hooks";
@@ -28,7 +28,7 @@ afterEach(() => {
function children(w: Component): Component[] { function children(w: Component): Component[] {
const childrenMap = w.__owl__.children; const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]); return Object.keys(childrenMap).map((id) => childrenMap[id]);
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -224,8 +224,8 @@ describe("t-slot directive", () => {
state = useState({ state = useState({
users: [ users: [
{ id: 1, name: "Aaron" }, { id: 1, name: "Aaron" },
{ id: 2, name: "David" } { id: 2, name: "David" },
] ],
}); });
static components = { Link }; static components = { Link };
} }
@@ -268,8 +268,8 @@ describe("t-slot directive", () => {
state = useState({ state = useState({
users: [ users: [
{ id: 1, name: "Aaron" }, { id: 1, name: "Aaron" },
{ id: 2, name: "David" } { id: 2, name: "David" },
] ],
}); });
static components = { Link }; static components = { Link };
} }
@@ -929,4 +929,193 @@ describe("t-slot directive", () => {
expect(fixture.innerHTML).toBe("<div><span>dash</span></div>"); expect(fixture.innerHTML).toBe("<div><span>dash</span></div>");
expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot();
}); });
test("slot and t-esc", async () => {
class Dialog extends Component {
static template = xml`<span><t t-slot="default"/></span>`;
}
class Parent extends Component {
static template = xml`<div><Dialog><t t-esc="'toph'"/></Dialog></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>toph</span></div>");
});
test("slot and (inline) t-esc", async () => {
class Dialog extends Component {
static template = xml`<span><t t-slot="default"/></span>`;
}
class Parent extends Component {
static template = xml`<div><Dialog t-esc="'toph'"/></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>toph</span></div>");
});
test("slot and t-call", async () => {
env.qweb.addTemplate("sokka", "<p>sokka</p>");
class Dialog extends Component {
static template = xml`<span><t t-slot="default"/></span>`;
}
class Parent extends Component {
static template = xml`<div><Dialog><t t-call="sokka"/></Dialog></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span><p>sokka</p></span></div>");
});
test("slot and (inline) t-call", async () => {
env.qweb.addTemplate("sokka", "<p>sokka</p>");
class Dialog extends Component {
static template = xml`<span><t t-slot="default"/></span>`;
}
class Parent extends Component {
static template = xml`<div><Dialog t-call="sokka"/></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span><p>sokka</p></span></div>");
});
test("named slot inside slot", async () => {
class Child extends Component {
static template = xml`
<div>
<t t-slot="brol"/>
<t t-slot="default"/>
</div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child>
<t t-set-slot="brol">
<p>A<t t-esc="value"/></p>
</t>
<Child>
<t t-set-slot="brol">
<p>B<t t-esc="value"/></p>
</t>
</Child>
</Child>
</div>`;
static components = { Child };
value = "blip";
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><p>Ablip</p><div><p>Bblip</p></div></div></div>");
});
test("named slots inside slot, again", async () => {
class Child extends Component {
static template = xml`
<child>
<t t-slot="brol1">default1</t>
<t t-slot="brol2">default2</t>
<t t-slot="default"/>
</child>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child>
<t t-set-slot="brol1">
<p>A<t t-esc="value"/></p>
</t>
<Child>
<t t-set-slot="brol2">
<p>B<t t-esc="value"/></p>
</t>
</Child>
</Child>
</div>`;
static components = { Child };
value = "blip";
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><child><p>Ablip</p>default2<child>default1<p>Bblip</p></child></child></div>"
);
});
test("named slot inside slot, part 3", async () => {
class Child extends Component {
static template = xml`
<div>
<t t-slot="brol"/>
<t t-slot="default"/>
</div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child>
<t t-set-slot="brol">
<p>A<t t-esc="value"/></p>
</t>
<Child>
<t>
<t t-set-slot="brol">
<p>B<t t-esc="value"/></p>
</t>
</t>
</Child>
</Child>
</div>`;
static components = { Child };
value = "blip";
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><p>Ablip</p><div><p>Bblip</p></div></div></div>");
});
test("dynamic t-slot call", async () => {
class Toggler extends Component {
static template = xml`<button t-on-click="toggle"><t t-slot="{{current.slot}}"/></button>`;
current = useState({ slot: "slot1" });
toggle() {
this.current.slot = this.current.slot === "slot1" ? "slot2" : "slot1";
}
}
class Parent extends Component {
static template = xml`
<div>
<Toggler>
<t t-set-slot="slot1"><p>slot1</p><span>content</span></t>
<t t-set-slot="slot2"><h1>slot2</h1></t>
</Toggler>
</div>`;
static components = { Toggler };
}
await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe("<div><button><p>slot1</p><span>content</span></button></div>");
fixture.querySelector<HTMLElement>("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button><h1>slot2</h1></button></div>");
fixture.querySelector<HTMLElement>("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button><p>slot1</p><span>content</span></button></div>");
expect(env.qweb.templates[Toggler.template].fn.toString()).toMatchSnapshot();
});
}); });
+128 -18
View File
@@ -1,7 +1,8 @@
import { Component, Env } from "../../src/component/component"; import { Component, Env, mount } from "../../src/component/component";
import { useState } from "../../src/hooks"; import { useState } from "../../src/hooks";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
import { makeDeferred, makeTestEnv, makeTestFixture, nextTick, nextMicroTick } from "../helpers"; import { makeDeferred, makeTestEnv, makeTestFixture, nextTick, nextMicroTick } from "../helpers";
import { scheduler } from "../../src/component/scheduler";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Setup and helpers // Setup and helpers
@@ -36,8 +37,7 @@ describe("mount targets", () => {
div.innerHTML = `<p>pre-existing</p>`; div.innerHTML = `<p>pre-existing</p>`;
fixture.appendChild(div); fixture.appendChild(div);
const app = new App(); const app = await mount(App, { target: div, position: "self" });
await app.mount(div, { position: "self" });
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
`<div class="arbitrary custom"><p>pre-existing</p>app<p>another tag</p></div>` `<div class="arbitrary custom"><p>pre-existing</p>app<p>another tag</p></div>`
@@ -65,10 +65,9 @@ describe("mount targets", () => {
const div = document.createElement("div"); const div = document.createElement("div");
fixture.appendChild(div); fixture.appendChild(div);
const app = new App();
let error; let error;
try { try {
await app.mount(div, { position: "self" }); await mount(App, { target: div, position: "self" });
} catch (e) { } catch (e) {
error = e; error = e;
} }
@@ -83,8 +82,7 @@ describe("mount targets", () => {
const span = document.createElement("span"); const span = document.createElement("span");
fixture.appendChild(span); fixture.appendChild(span);
const app = new App(); await mount(App, { target: fixture, position: "first-child" });
await app.mount(fixture, { position: "first-child" });
expect(fixture.innerHTML).toBe("<div>app</div><span></span>"); expect(fixture.innerHTML).toBe("<div>app</div><span></span>");
}); });
@@ -95,8 +93,7 @@ describe("mount targets", () => {
const span = document.createElement("span"); const span = document.createElement("span");
fixture.appendChild(span); fixture.appendChild(span);
const app = new App(); await mount(App, { target: fixture, position: "last-child" });
await app.mount(fixture, { position: "last-child" });
expect(fixture.innerHTML).toBe("<span></span><div>app</div>"); expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
}); });
@@ -107,8 +104,7 @@ describe("mount targets", () => {
const span = document.createElement("span"); const span = document.createElement("span");
fixture.appendChild(span); fixture.appendChild(span);
const app = new App(); await mount(App, { target: fixture });
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<span></span><div>app</div>"); expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
}); });
}); });
@@ -132,8 +128,7 @@ describe("unmounting and remounting", () => {
} }
} }
const w = new MyWidget(); const w = await mount(MyWidget, { target: fixture });
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>"); expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]); expect(steps).toEqual(["willstart", "mounted"]);
@@ -161,8 +156,7 @@ describe("unmounting and remounting", () => {
} }
} }
const w = new MyWidget(); const w = await mount(MyWidget, { target: fixture });
await w.mount(fixture);
await w.mount(fixture); await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>"); expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]); expect(steps).toEqual(["willstart", "mounted"]);
@@ -202,8 +196,7 @@ describe("unmounting and remounting", () => {
state = useState({ val: 1, flag: true }); state = useState({ val: 1, flag: true });
} }
const widget = new Parent(); const widget = await mount(Parent, { target: fixture });
await widget.mount(fixture);
expect(steps).toEqual(["render"]); expect(steps).toEqual(["render"]);
expect(fixture.innerHTML).toBe("<div><span>12</span></div>"); expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
widget.state.flag = false; widget.state.flag = false;
@@ -335,7 +328,7 @@ describe("unmounting and remounting", () => {
class Child extends Component { class Child extends Component {
static template = xml`<span t-esc="state.val"/>`; static template = xml`<span t-esc="state.val"/>`;
state = useState({ state = useState({
val: "C1" val: "C1",
}); });
constructor(parent, props) { constructor(parent, props) {
super(parent, props); super(parent, props);
@@ -490,4 +483,121 @@ describe("unmounting and remounting", () => {
expect(steps).toEqual(["1 resolved", "2 resolved"]); expect(steps).toEqual(["1 resolved", "2 resolved"]);
expect(fixture.innerHTML).toBe("<div>Hey</div>"); expect(fixture.innerHTML).toBe("<div>Hey</div>");
}); });
test("mounting a destroyed widget", async () => {
class MyWidget extends Component {
static template = xml`<div>Hey</div>`;
}
const w = new MyWidget();
w.destroy(); // because, why not
let error;
try {
await w.mount(fixture);
} catch (e) {
error = e;
}
expect(scheduler.tasks.length).toBe(0);
expect(error).toBeDefined();
expect(error.message).toBe("Cannot mount a destroyed component");
});
test("destroying a sub-component cleans itself from parent's vnode", async () => {
class C1 extends Component {
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
}
class P extends Component {
static components = { C1 };
static template = xml`<div><div><C1 t-props="state" t-if="state.a"/></div></div>`;
state = {
a: "first",
};
}
const parent = new P();
await parent.mount(fixture);
expect(fixture.textContent).toBe("first");
parent.unmount();
parent.state.a = "";
parent.mount(fixture);
parent.state.a = "fixed";
await parent.render();
expect(fixture.textContent).toBe("fixed");
});
test("destroying a sub-component cleans itself from parent's vnode, part 2", async () => {
class C1 extends Component {
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
}
class P extends Component {
static components = { C1 };
static template = xml`<div><div><C1 t-props="state" t-if="state.a"/>some text</div></div>`;
state = {
a: "first",
};
}
const parent = new P();
await parent.mount(fixture);
expect(fixture.textContent).toBe("firstsome text");
parent.unmount();
parent.state.a = "";
parent.mount(fixture);
parent.state.a = "fixed";
await parent.render();
expect(fixture.textContent).toBe("fixedsome text");
});
test("destroying a sub-component cleans itself from parent's vnode, part 3", async () => {
class C1 extends Component {
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
}
class C2 extends Component {
static template = xml`<C1 a="props.a"/>`;
static components = { C1 };
}
class P extends Component {
static components = { C2 };
static template = xml`<div><div><C2 t-props="state" t-if="state.a"/></div></div>`;
state = {
a: "first",
};
}
const parent = new P();
await parent.mount(fixture);
expect(fixture.textContent).toBe("first");
parent.unmount();
parent.state.a = "";
parent.mount(fixture);
parent.state.a = "fixed";
await parent.render();
expect(fixture.textContent).toBe("fixed");
});
test("destroying a sub-component cleans itself from parent's vnode, part 4", async () => {
class C1 extends Component {
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
}
class C2 extends Component {
static template = xml`<C1 a="props.a"/>`;
static components = { C1 };
}
class P extends Component {
static components = { C2 };
static template = xml`<div><div><C2 t-props="state" t-if="state.a"/>some text</div></div>`;
state = {
a: "first",
};
}
const parent = new P();
await parent.mount(fixture);
expect(fixture.textContent).toBe("firstsome text");
parent.unmount();
parent.state.a = "";
parent.mount(fixture);
parent.state.a = "fixed";
await parent.render();
expect(fixture.textContent).toBe("fixedsome text");
});
}); });
+1 -1
View File
@@ -14,7 +14,7 @@ describe("event bus behaviour", () => {
expect.assertions(1); expect.assertions(1);
const bus = new EventBus(); const bus = new EventBus();
const owner = {}; const owner = {};
bus.on("event", owner, function(this: any) { bus.on("event", owner, function (this: any) {
expect(this).toBe(owner); expect(this).toBe(owner);
}); });
bus.trigger("event"); bus.trigger("event");
+19 -2
View File
@@ -22,7 +22,7 @@ describe("observer", () => {
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
expect(obj2).toEqual({ expect(obj2).toEqual({
a: 2 a: 2,
}); });
}); });
@@ -47,7 +47,7 @@ describe("observer", () => {
expect(observer.rev).toBe(5); expect(observer.rev).toBe(5);
expect(obj).toEqual({ expect(obj).toEqual({
a: null, a: null,
b: undefined b: undefined,
}); });
}); });
@@ -68,6 +68,23 @@ describe("observer", () => {
expect(obj.date).not.toBe(date); expect(obj.date).not.toBe(date);
}); });
test("properly handle promises (i.e.: treat them like primitive values", async () => {
const observer = new Observer();
let resolved = false;
const prom = new Promise((r) => r());
const obj: any = observer.observe({ prom });
expect(obj.prom).toBeInstanceOf(Promise);
obj.prom.then(() => (resolved = true));
expect(observer.revNumber(obj)).toBe(1);
expect(resolved).toBe(false);
await Promise.resolve();
expect(resolved).toBe(true);
expect(observer.revNumber(obj)).toBe(1);
});
test("can change values in array", () => { test("can change values in array", () => {
const observer = new Observer(); const observer = new Observer();
const obj: any = observer.observe({ arr: [1, 2] }); const obj: any = observer.observe({ arr: [1, 2] });
+10 -12
View File
@@ -8,12 +8,6 @@ import "../src/qweb/extensions";
import "../src/component/directive"; import "../src/component/directive";
import { browser } from "../src/browser"; import { browser } from "../src/browser";
// modifies scheduler to make it faster to test components
scheduler.requestAnimationFrame = function(callback: FrameRequestCallback) {
setTimeout(callback, 1);
return 1;
};
// Some static cleanup // Some static cleanup
let nextSlotId; let nextSlotId;
let slots; let slots;
@@ -43,9 +37,13 @@ export function nextMicroTick(): Promise<void> {
} }
export async function nextTick(): Promise<void> { export async function nextTick(): Promise<void> {
return new Promise(function(resolve) { await new Promise((resolve) => setTimeout(resolve));
setTimeout(() => scheduler.requestAnimationFrame(() => resolve())); await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
}); }
export async function nextFrame(): Promise<void> {
await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
} }
export function makeTestFixture() { export function makeTestFixture() {
@@ -77,7 +75,7 @@ export function makeDeferred(): Deferred {
export function makeTestEnv(): Env { export function makeTestEnv(): Env {
return { return {
qweb: new QWeb(), qweb: new QWeb(),
browser: browser browser: browser,
}; };
} }
@@ -134,7 +132,7 @@ export function renderToString(
// is useful for animations tests, as we hook before repaints to trigger // is useful for animations tests, as we hook before repaints to trigger
// animations (thanks to requestAnimationFrame). Patching nextFrame allows to // animations (thanks to requestAnimationFrame). Patching nextFrame allows to
// simulate calls to this hook. One must not forget to unpatch afterwards. // simulate calls to this hook. One must not forget to unpatch afterwards.
let nextFrame = QWeb.utils.nextFrame; let _nextFrame = QWeb.utils.nextFrame;
export function patchNextFrame(f: Function) { export function patchNextFrame(f: Function) {
QWeb.utils.nextFrame = (cb: () => void) => { QWeb.utils.nextFrame = (cb: () => void) => {
setTimeout(() => f(cb)); setTimeout(() => f(cb));
@@ -142,7 +140,7 @@ export function patchNextFrame(f: Function) {
} }
export function unpatchNextFrame() { export function unpatchNextFrame() {
QWeb.utils.nextFrame = nextFrame; QWeb.utils.nextFrame = _nextFrame;
} }
export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) { export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) {
+7 -7
View File
@@ -10,7 +10,7 @@ import {
onWillStart, onWillStart,
onWillUpdateProps, onWillUpdateProps,
useSubEnv, useSubEnv,
useExternalListener useExternalListener,
} from "../src/hooks"; } from "../src/hooks";
import { xml } from "../src/tags"; import { xml } from "../src/tags";
@@ -214,7 +214,7 @@ describe("hooks", () => {
"hook:mounted1", "hook:mounted1",
"hook:mounted2", "hook:mounted2",
"hook:willunmount2", "hook:willunmount2",
"hook:willunmount1" "hook:willunmount1",
]); ]);
}); });
@@ -267,7 +267,7 @@ describe("hooks", () => {
class WidgetC extends Component { class WidgetC extends Component {
static template = xml`<div class="outer-div">Hello<WidgetB t-ref="mywidgetb" /></div>`; static template = xml`<div class="outer-div">Hello<WidgetB t-ref="mywidgetb" /></div>`;
static components = { WidgetB }; static components = { WidgetB };
ref = useRef("mywidgetb"); ref = useRef<WidgetB>("mywidgetb");
} }
const widget = new WidgetC(); const widget = new WidgetC();
@@ -560,7 +560,7 @@ describe("hooks", () => {
test("can use onWillStart, onWillUpdateProps", async () => { test("can use onWillStart, onWillUpdateProps", async () => {
const steps: string[] = []; const steps: string[] = [];
async function slow(): Promise<string> { async function slow(): Promise<string> {
return new Promise(resolve => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
resolve("slow"); resolve("slow");
}, 0); }, 0);
@@ -571,7 +571,7 @@ describe("hooks", () => {
steps.push(await slow()); steps.push(await slow());
steps.push("onWillStart"); steps.push("onWillStart");
}); });
onWillUpdateProps(async nextProps => { onWillUpdateProps(async (nextProps) => {
expect(nextProps).toEqual({ value: 2 }); expect(nextProps).toEqual({ value: 2 });
steps.push(await slow()); steps.push(await slow());
steps.push("onWillUpdateProps"); steps.push("onWillUpdateProps");
@@ -581,7 +581,7 @@ describe("hooks", () => {
onWillStart(() => { onWillStart(() => {
steps.push("on2ndStart"); steps.push("on2ndStart");
}); });
onWillUpdateProps(nextProps => { onWillUpdateProps((nextProps) => {
expect(nextProps).toEqual({ value: 2 }); expect(nextProps).toEqual({ value: 2 });
steps.push("on2ndUpdate"); steps.push("on2ndUpdate");
}); });
@@ -623,7 +623,7 @@ describe("hooks", () => {
"onWillStart", "onWillStart",
"on2ndUpdate", "on2ndUpdate",
"slow", "slow",
"onWillUpdateProps" "onWillUpdateProps",
]); ]);
}); });
+4 -4
View File
@@ -486,7 +486,7 @@ describe("Portal: Basic use and DOM placement", () => {
"parent:mounted", "parent:mounted",
"parent:willPatch", "parent:willPatch",
"child:mounted", "child:mounted",
"parent:patched" "parent:patched",
]); ]);
parent.state.val = 2; parent.state.val = 2;
@@ -499,7 +499,7 @@ describe("Portal: Basic use and DOM placement", () => {
"parent:willPatch", "parent:willPatch",
"child:willPatch", "child:willPatch",
"child:patched", "child:patched",
"parent:patched" "parent:patched",
]); ]);
parent.state.hasChild = false; parent.state.hasChild = false;
@@ -515,7 +515,7 @@ describe("Portal: Basic use and DOM placement", () => {
"parent:patched", "parent:patched",
"parent:willPatch", "parent:willPatch",
"child:willUnmount", "child:willUnmount",
"parent:patched" "parent:patched",
]); ]);
}); });
@@ -741,7 +741,7 @@ describe("Portal: Events handling", () => {
steps.push(ev.type as string); steps.push(ev.type as string);
} }
} }
const bodyListener = ev => { const bodyListener = (ev) => {
steps.push(`body: ${ev.type}`); steps.push(`body: ${ev.type}`);
}; };
document.body.addEventListener("click", bodyListener); document.body.addEventListener("click", bodyListener);
File diff suppressed because it is too large Load Diff
+210 -55
View File
@@ -195,7 +195,7 @@ describe("t-esc", () => {
v2: undefined, v2: undefined,
v3: null, v3: null,
v4: 0, v4: 0,
v5: "" v5: "",
}; };
expect(renderToString(qweb, "test", vals)).toBe( expect(renderToString(qweb, "test", vals)).toBe(
"<div><p>false</p><p></p><p></p><p>0</p><p></p></div>" "<div><p>false</p><p></p><p></p><p>0</p><p></p></div>"
@@ -207,6 +207,13 @@ describe("t-esc", () => {
const result = renderToString(qweb, "test", { state: { list: [1, 2] } }); const result = renderToString(qweb, "test", { state: { list: [1, 2] } });
expect(result).toBe("<span>1,2</span>"); expect(result).toBe("<span>1,2</span>");
}); });
test("t-esc inside t-call, with t-set outside", () => {
qweb.addTemplate("main", `<div><t t-set="v">Hi</t><t t-call="sub"/></div>`);
qweb.addTemplate("sub", `<span t-esc="v"/>`);
const result = renderToString(qweb, "main");
expect(result).toBe("<div><span>Hi</span></div>");
});
}); });
describe("t-raw", () => { describe("t-raw", () => {
@@ -231,6 +238,13 @@ describe("t-raw", () => {
"<span><span>hello</span><ok>world</ok></span>" "<span><span>hello</span><ok>world</ok></span>"
); );
}); });
test("t-raw with comment", () => {
qweb.addTemplate("test", `<span><t t-raw="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<p>text<!-- top secret --></p>" })).toBe(
"<span><p>text<!-- top secret --></p></span>"
);
});
}); });
describe("t-set", () => { describe("t-set", () => {
@@ -486,7 +500,7 @@ describe("t-if", () => {
cond3: false, cond3: false,
cond4: false, cond4: false,
m: 5, m: 5,
n: 2 n: 2,
}; };
expect(normalize(renderToString(qweb, "test", context))).toBe("<div>andormgtnlt</div>"); expect(normalize(renderToString(qweb, "test", context))).toBe("<div>andormgtnlt</div>");
}); });
@@ -625,7 +639,7 @@ describe("attributes", () => {
test("object", () => { test("object", () => {
qweb.addTemplate("test", `<div t-att="value"/>`); qweb.addTemplate("test", `<div t-att="value"/>`);
const result = renderToString(qweb, "test", { const result = renderToString(qweb, "test", {
value: { a: 1, b: 2, c: 3 } value: { a: 1, b: 2, c: 3 },
}); });
expect(result).toBe(`<div a="1" b="2" c="3"></div>`); expect(result).toBe(`<div a="1" b="2" c="3"></div>`);
}); });
@@ -684,7 +698,7 @@ describe("attributes", () => {
const result = renderToString(qweb, "test", { const result = renderToString(qweb, "test", {
value1: 0, value1: 0,
value2: 1, value2: 1,
value3: 2 value3: 2,
}); });
expect(result).toBe(`<div foo="a 0 is 1 of 2 ]"></div>`); expect(result).toBe(`<div foo="a 0 is 1 of 2 ]"></div>`);
}); });
@@ -703,7 +717,7 @@ describe("attributes", () => {
const result = renderToString(qweb, "test", { const result = renderToString(qweb, "test", {
bar: 0, bar: 0,
baz: 1, baz: 1,
qux: { qux: "<>" } qux: { qux: "<>" },
}); });
const expected = '<div foo="<foo" bar="0" baz="<1>" qux="<>"></div>'; const expected = '<div foo="<foo" bar="0" baz="<1>" qux="<>"></div>';
expect(result).toBe(expected); expect(result).toBe(expected);
@@ -715,6 +729,12 @@ describe("attributes", () => {
expect(result).toBe(`<div class="hello world"></div>`); expect(result).toBe(`<div class="hello world"></div>`);
}); });
test("class and t-att-class should combine together", () => {
qweb.addTemplate("test", `<div t-att-class="value" class="hello" />`);
const result = renderToString(qweb, "test", { value: "world" });
expect(result).toBe(`<div class="world hello"></div>`);
});
test("class and t-attf-class with ternary operation", () => { test("class and t-attf-class with ternary operation", () => {
qweb.addTemplate("test", `<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`); qweb.addTemplate("test", `<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`);
const result = renderToString(qweb, "test", { value: true }); const result = renderToString(qweb, "test", { value: true });
@@ -734,7 +754,7 @@ describe("t-call (template calling", () => {
qweb.addTemplate("caller", '<div><t t-call="_basic-callee"/></div>'); qweb.addTemplate("caller", '<div><t t-call="_basic-callee"/></div>');
const expected = "<div><span>ok</span></div>"; const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller")).toBe(expected); expect(renderToString(qweb, "caller")).toBe(expected);
expect(qweb.subTemplates["_basic-callee"].toString()).toMatchSnapshot(); expect(qweb.subTemplates["_basic-callee"]).toBeTruthy();
}); });
test("basic caller, no parent node", () => { test("basic caller, no parent node", () => {
@@ -742,7 +762,7 @@ describe("t-call (template calling", () => {
qweb.addTemplate("caller", '<t t-call="_basic-callee"/>'); qweb.addTemplate("caller", '<t t-call="_basic-callee"/>');
const expected = "<div>ok</div>"; const expected = "<div>ok</div>";
expect(renderToString(qweb, "caller")).toBe(expected); expect(renderToString(qweb, "caller")).toBe(expected);
expect(qweb.subTemplates["_basic-callee"].toString()).toMatchSnapshot(); expect(qweb.subTemplates["_basic-callee"]).toBeTruthy();
}); });
test("t-call with t-if", () => { test("t-call with t-if", () => {
@@ -750,13 +770,14 @@ describe("t-call (template calling", () => {
qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>'); qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>');
const expected = "<div><span>ok</span></div>"; const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller", { flag: true })).toBe(expected); expect(renderToString(qweb, "caller", { flag: true })).toBe(expected);
expect(qweb.subTemplates["sub"].toString()).toMatchSnapshot(); expect(qweb.subTemplates["sub"]).toBeTruthy();
}); });
test("t-call not allowed on a non t node", () => { test("t-call allowed on a non t node", () => {
qweb.addTemplate("_basic-callee", "<t>ok</t>"); qweb.addTemplate("_basic-callee", "<span>ok</span>");
qweb.addTemplate("caller", '<div t-call="_basic-callee"/>'); qweb.addTemplate("caller", '<div t-call="_basic-callee"/>');
expect(() => renderToString(qweb, "caller")).toThrow("Invalid tag"); const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller")).toBe(expected);
}); });
test("with unused body", () => { test("with unused body", () => {
@@ -885,7 +906,8 @@ describe("t-call (template calling", () => {
`); `);
const expected = "<div><span>hey</span></div>"; const expected = "<div><span>hey</span></div>";
expect(renderToString(qweb, "recursive")).toBe(expected); expect(renderToString(qweb, "recursive")).toBe(expected);
const recursiveFn = Object.values(qweb.subTemplates)[0] as any; const subId = qweb.subTemplates["recursive"];
const recursiveFn = QWeb.subTemplates[subId] as any;
expect(recursiveFn.toString()).toMatchSnapshot(); expect(recursiveFn.toString()).toMatchSnapshot();
}); });
@@ -913,7 +935,8 @@ describe("t-call (template calling", () => {
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe( expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe(
expected expected
); );
const recursiveFn = Object.values(qweb.subTemplates)[0] as any; const subId = qweb.subTemplates["nodeTemplate"];
const recursiveFn = QWeb.subTemplates[subId] as any;
expect(recursiveFn.toString()).toMatchSnapshot(); expect(recursiveFn.toString()).toMatchSnapshot();
}); });
@@ -940,7 +963,8 @@ describe("t-call (template calling", () => {
const expected = const expected =
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>"; "<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>";
expect(renderToString(qweb, "Parent", { root }, { fiber: {} })).toBe(expected); expect(renderToString(qweb, "Parent", { root }, { fiber: {} })).toBe(expected);
const recursiveFn = Object.values(qweb.subTemplates)[0] as any; const subId = qweb.subTemplates["nodeTemplate"];
const recursiveFn = QWeb.subTemplates[subId] as any;
expect(recursiveFn.toString()).toMatchSnapshot(); expect(recursiveFn.toString()).toMatchSnapshot();
}); });
@@ -967,12 +991,13 @@ describe("t-call (template calling", () => {
`); `);
const root = { const root = {
val: "a", val: "a",
children: [{ val: "b", children: [{ val: "c", children: [{ val: "d" }] }] }] children: [{ val: "b", children: [{ val: "c", children: [{ val: "d" }] }] }],
}; };
const expected = const expected =
"<div><div><p>a 2</p><div><p>b 3</p><div><p>c 4</p><div><p>d 5</p></div></div></div></div></div>"; "<div><div><p>a 2</p><div><p>b 3</p><div><p>c 4</p><div><p>d 5</p></div></div></div></div></div>";
expect(renderToString(qweb, "Parent", { root })).toBe(expected); expect(renderToString(qweb, "Parent", { root })).toBe(expected);
const recursiveFn = Object.values(qweb.subTemplates)[0] as any; const subId = qweb.subTemplates["nodeTemplate"];
const recursiveFn = QWeb.subTemplates[subId] as any;
expect(recursiveFn.toString()).toMatchSnapshot(); expect(recursiveFn.toString()).toMatchSnapshot();
}); });
@@ -1044,6 +1069,52 @@ describe("t-call (template calling", () => {
const context = { list: [{ val: 1 }, { val: 2 }, { val: 3 }] }; const context = { list: [{ val: 1 }, { val: 2 }, { val: 3 }] };
expect(trim(renderToString(qweb, "wrapper", context))).toBe(expected); expect(trim(renderToString(qweb, "wrapper", context))).toBe(expected);
}); });
test("two different QWeb instances, and shared templates", () => {
QWeb.registerTemplate("sub", `<span>ok</span>`);
QWeb.registerTemplate("main", `<div><t t-call="sub"/></div>`);
const qweb1 = new QWeb();
const qweb2 = new QWeb();
expect(renderToString(qweb1, "main")).toBe("<div><span>ok</span></div>");
expect(renderToString(qweb2, "main")).toBe("<div><span>ok</span></div>");
});
test("t-call with t-set inside and body text content", () => {
qweb.addTemplate("sub", `<p><t t-esc="val"/></p>`);
qweb.addTemplate(
"main",
`
<div>
<t t-call="sub">
<t t-set="val">yip yip</t>
</t>
</div>`
);
const expected = "<div><p>yip yip</p></div>";
expect(renderToString(qweb, "main")).toBe(expected);
});
test("t-call with body content as root of a template", () => {
qweb.addTemplate("antony", `<foo><t t-raw="0"/></foo>`);
qweb.addTemplate("main", `<t><t t-call="antony"><p>antony</p></t></t>`);
const expected = "<foo><p>antony</p></foo>";
expect(renderToString(qweb, "main")).toBe(expected);
});
test("dynamic t-call", () => {
qweb.addTemplate("foo", `<foo><t t-esc="val"/></foo>`);
qweb.addTemplate("bar", `<bar><t t-esc="val"/></bar>`);
qweb.addTemplate("main", `<div><t t-call="{{template}}"/></div>`);
const expected = "<div><foo>foo</foo></div>";
expect(renderToString(qweb, "main", { template: "foo", val: "foo" })).toBe(expected);
const expected2 = "<div><bar>quux</bar></div>";
expect(renderToString(qweb, "main", { template: "bar", val: "quux" })).toBe(expected2);
// duplicate call because there was a specific bug with some id that was
// incremented each rendering.
expect(renderToString(qweb, "main", { template: "bar", val: "quux" })).toBe(expected2);
});
}); });
describe("foreach", () => { describe("foreach", () => {
@@ -1134,6 +1205,63 @@ describe("foreach", () => {
); );
}); });
test("t-call without body in t-foreach in t-foreach", () => {
qweb.addTemplate(
"test_called",
`<t>
<t t-set="c" t-value="'x' + '_' + a + '_'+ b" />
[<t t-esc="a" />]
[<t t-esc="b" />]
[<t t-esc="c" />]
</t>`
);
qweb.addTemplate(
"test",
`<div>
<t t-foreach="numbers" t-as="a">
<t t-foreach="letters" t-as="b">
<t t-call="test_called" />
</t>
<span t-esc="c"/>
</t>
<span>[<t t-esc="a" />][<t t-esc="b" />][<t t-esc="c" />]</span>
</div>`
);
const context = { numbers: [1, 2, 3], letters: ["a", "b"] };
expect(renderToString(qweb, "test", context)).toBe(
"<div> [1] [a] [x_1_a] [1] [b] [x_1_b] <span></span> [2] [a] [x_2_a] [2] [b] [x_2_b] <span></span> [3] [a] [x_3_a] [3] [b] [x_3_b] <span></span><span>[][][]</span></div>"
);
});
test("t-call with body in t-foreach in t-foreach", () => {
qweb.addTemplate(
"test_called",
`<t>
[<t t-esc="a" />]
[<t t-esc="b" />]
[<t t-esc="c" />]
</t>`
);
qweb.addTemplate(
"test",
`<div>
<t t-foreach="numbers" t-as="a">
<t t-foreach="letters" t-as="b">
<t t-call="test_called" >
<t t-set="c" t-value="'x' + '_' + a + '_'+ b" />
</t>
</t>
<span t-esc="c"/>
</t>
<span>[<t t-esc="a" />][<t t-esc="b" />][<t t-esc="c" />]</span>
</div>`
);
const context = { numbers: [1, 2, 3], letters: ["a", "b"] };
expect(renderToString(qweb, "test", context)).toBe(
"<div> [1] [a] [x_1_a] [1] [b] [x_1_b] <span></span> [2] [a] [x_2_a] [2] [b] [x_2_b] <span></span> [3] [a] [x_3_a] [3] [b] [x_3_b] <span></span><span>[][][]</span></div>"
);
});
test("throws error if invalid loop expression", () => { test("throws error if invalid loop expression", () => {
qweb.addTemplate( qweb.addTemplate(
"test", "test",
@@ -1229,7 +1357,7 @@ describe("t-on", () => {
{ {
add() { add() {
a = 3; a = 3;
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1252,7 +1380,7 @@ describe("t-on", () => {
}, },
handleDblClick() { handleDblClick() {
steps.push("dblclick"); steps.push("dblclick");
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1272,7 +1400,7 @@ describe("t-on", () => {
{ {
add(n) { add(n) {
a = a + n; a = a + n;
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1289,7 +1417,7 @@ describe("t-on", () => {
{ {
add({ val }) { add({ val }) {
a = a + val; a = a + val;
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1306,7 +1434,7 @@ describe("t-on", () => {
{ {
doSomething(arg) { doSomething(arg) {
expect(arg).toEqual({}); expect(arg).toEqual({});
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1322,7 +1450,7 @@ describe("t-on", () => {
{ {
doSomething(arg) { doSomething(arg) {
expect(arg).toEqual({}); expect(arg).toEqual({});
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1344,7 +1472,7 @@ describe("t-on", () => {
{ {
activate(action) { activate(action) {
expect(action).toBe("someval"); expect(action).toBe("someval");
} },
}, },
{ handlers: [] } { handlers: [] }
); );
@@ -1357,7 +1485,7 @@ describe("t-on", () => {
let owner = { let owner = {
add() { add() {
expect(this).toBe(owner); expect(this).toBe(owner);
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
(<HTMLElement>node).click(); (<HTMLElement>node).click();
@@ -1367,8 +1495,8 @@ describe("t-on", () => {
qweb.addTemplate("test", `<button t-on-click="state.counter++">Click</button>`); qweb.addTemplate("test", `<button t-on-click="state.counter++">Click</button>`);
let owner = { let owner = {
state: { state: {
counter: 0 counter: 0,
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.counter).toBe(0); expect(owner.state.counter).toBe(0);
@@ -1381,10 +1509,10 @@ describe("t-on", () => {
let owner = { let owner = {
state: { state: {
counter: 0, counter: 0,
incrementCounter: inc => { incrementCounter: (inc) => {
owner.state.counter += inc; owner.state.counter += inc;
} },
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.counter).toBe(0); expect(owner.state.counter).toBe(0);
@@ -1396,8 +1524,8 @@ describe("t-on", () => {
qweb.addTemplate("test", `<button t-on-click="state.flag = !state.flag">Toggle</button>`); qweb.addTemplate("test", `<button t-on-click="state.flag = !state.flag">Toggle</button>`);
let owner = { let owner = {
state: { state: {
flag: true flag: true,
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.flag).toBe(true); expect(owner.state.flag).toBe(true);
@@ -1414,8 +1542,8 @@ describe("t-on", () => {
return n + 1; return n + 1;
}, },
state: { state: {
n: 11 n: 11,
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.n).toBe(11); expect(owner.state.n).toBe(11);
@@ -1431,7 +1559,7 @@ describe("t-on", () => {
let owner = { let owner = {
update() { update() {
expect(this).toBe(owner); expect(this).toBe(owner);
} },
}; };
const node = renderToDOM(qweb, "main", owner, { handlers: [] }); const node = renderToDOM(qweb, "main", owner, { handlers: [] });
@@ -1448,7 +1576,7 @@ describe("t-on", () => {
expect(this).toBe(owner); expect(this).toBe(owner);
expect(val).toBe(444); expect(val).toBe(444);
}, },
value: 444 value: 444,
}; };
const node = renderToDOM(qweb, "main", owner, { handlers: [] }); const node = renderToDOM(qweb, "main", owner, { handlers: [] });
@@ -1477,7 +1605,7 @@ describe("t-on", () => {
onClickPreventedAndStopped(e) { onClickPreventedAndStopped(e) {
expect(e.defaultPrevented).toBe(true); expect(e.defaultPrevented).toBe(true);
expect(e.cancelBubble).toBe(true); expect(e.cancelBubble).toBe(true);
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1503,7 +1631,7 @@ describe("t-on", () => {
}, },
onClickSelf(e) { onClickSelf(e) {
steps.push("onClickSelf"); steps.push("onClickSelf");
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1527,10 +1655,10 @@ describe("t-on", () => {
); );
let steps: boolean[] = []; let steps: boolean[] = [];
let owner = { let owner = {
onClick() {} onClick() {},
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
(<HTMLElement>node).addEventListener("click", function(e) { (<HTMLElement>node).addEventListener("click", function (e) {
steps.push(e.defaultPrevented); steps.push(e.defaultPrevented);
}); });
@@ -1552,10 +1680,10 @@ describe("t-on", () => {
); );
let steps: boolean[] = []; let steps: boolean[] = [];
let owner = { let owner = {
onClick() {} onClick() {},
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
(<HTMLElement>node).addEventListener("click", function(e) { (<HTMLElement>node).addEventListener("click", function (e) {
steps.push(e.defaultPrevented); steps.push(e.defaultPrevented);
}); });
@@ -1583,13 +1711,13 @@ describe("t-on", () => {
const owner = { const owner = {
projects: [ projects: [
{ id: 1, name: "Project 1" }, { id: 1, name: "Project 1" },
{ id: 2, name: "Project 2" } { id: 2, name: "Project 2" },
], ],
onEdit(projectId, ev) { onEdit(projectId, ev) {
expect(ev.defaultPrevented).toBe(true); expect(ev.defaultPrevented).toBe(true);
steps.push(projectId); steps.push(projectId);
} },
}; };
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] }); const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1614,7 +1742,7 @@ describe("t-on", () => {
); );
const node = renderToDOM(qweb, "test", {}, { handlers: [] }); const node = renderToDOM(qweb, "test", {}, { handlers: [] });
node.addEventListener("click", e => { node.addEventListener("click", (e) => {
expect(e.defaultPrevented).toBe(true); expect(e.defaultPrevented).toBe(true);
}); });
@@ -1630,7 +1758,7 @@ describe("t-on", () => {
text: "Click here", text: "Click here",
onClick() { onClick() {
steps.push("onClick"); steps.push("onClick");
} },
}; };
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] }); const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1649,7 +1777,7 @@ describe("t-on", () => {
html: "Click <b>here</b>", html: "Click <b>here</b>",
onClick() { onClick() {
steps.push("onClick"); steps.push("onClick");
} },
}; };
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] }); const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1676,7 +1804,7 @@ describe("t-on", () => {
}, },
doSomething() { doSomething() {
steps.push("normal"); steps.push("normal");
} },
}; };
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
@@ -1749,7 +1877,7 @@ describe("loading templates", () => {
}); });
}); });
describe("special cases for some boolean html attributes/properties", () => { describe("special cases for some specific html attributes/properties", () => {
test("input type= checkbox, with t-att-checked", () => { test("input type= checkbox, with t-att-checked", () => {
qweb.addTemplate("test", `<input type="checkbox" t-att-checked="flag"/>`); qweb.addTemplate("test", `<input type="checkbox" t-att-checked="flag"/>`);
const result = renderToString(qweb, "test", { flag: true }); const result = renderToString(qweb, "test", { flag: true });
@@ -1776,6 +1904,33 @@ describe("special cases for some boolean html attributes/properties", () => {
); );
renderToString(qweb, "test", { flag: true }); renderToString(qweb, "test", { flag: true });
}); });
test("input with t-att-value", () => {
// render input with initial value
qweb.addTemplate("test", `<input t-att-value="v"/>`);
const vnode1 = qweb.render("test", { v: "zucchini" });
const vnode2 = patch(document.createElement("input"), vnode1);
let elm = vnode2.elm as HTMLInputElement;
expect(elm.value).toBe("zucchini");
// change value manually in input, to simulate user input
elm.value = "tomato";
expect(elm.value).toBe("tomato");
// rerender with a different value, and patch actual dom, to check that
// input value was properly reset by owl
const vnode3 = qweb.render("test", { v: "potato" });
patch(vnode2, vnode3);
expect(elm.value).toBe("potato");
});
test("input of type checkbox with t-att-indeterminate", () => {
qweb.addTemplate("test", `<input type="checkbox" t-att-indeterminate="v"/>`);
const vnode1 = qweb.render("test", { v: true });
const vnode2 = patch(document.createElement("input"), vnode1);
let elm = vnode2.elm as HTMLInputElement;
expect(elm.indeterminate).toBe(true);
});
}); });
describe("whitespace handling", () => { describe("whitespace handling", () => {
@@ -1840,7 +1995,7 @@ describe("t-key", () => {
); );
expect( expect(
renderToString(qweb, "test", { renderToString(qweb, "test", {
beers: [{ id: 12, name: "Chimay Rouge" }] beers: [{ id: 12, name: "Chimay Rouge" }],
}) })
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
@@ -1953,9 +2108,9 @@ describe("properly support svg", () => {
describe("translation support", () => { describe("translation support", () => {
test("can translate node content", () => { test("can translate node content", () => {
const translations = { const translations = {
word: "mot" word: "mot",
}; };
const translateFn = expr => translations[expr] || expr; const translateFn = (expr) => translations[expr] || expr;
const qweb = new QWeb({ translateFn }); const qweb = new QWeb({ translateFn });
qweb.addTemplate("test", "<div>word</div>"); qweb.addTemplate("test", "<div>word</div>");
expect(renderToString(qweb, "test")).toBe("<div>mot</div>"); expect(renderToString(qweb, "test")).toBe("<div>mot</div>");
@@ -1963,9 +2118,9 @@ describe("translation support", () => {
test("does not translate node content if disabled", () => { test("does not translate node content if disabled", () => {
const translations = { const translations = {
word: "mot" word: "mot",
}; };
const translateFn = expr => translations[expr] || expr; const translateFn = (expr) => translations[expr] || expr;
const qweb = new QWeb({ translateFn }); const qweb = new QWeb({ translateFn });
qweb.addTemplate( qweb.addTemplate(
"test", "test",
@@ -1980,9 +2135,9 @@ describe("translation support", () => {
test("some attributes are translated", () => { test("some attributes are translated", () => {
const translations = { const translations = {
word: "mot" word: "mot",
}; };
const translateFn = expr => translations[expr] || expr; const translateFn = (expr) => translations[expr] || expr;
const qweb = new QWeb({ translateFn }); const qweb = new QWeb({ translateFn });
qweb.addTemplate( qweb.addTemplate(
"test", "test",
+10 -10
View File
@@ -6,12 +6,12 @@ describe("tokenizer", () => {
expect(tokenize("{}")).toEqual([ expect(tokenize("{}")).toEqual([
{ type: "LEFT_BRACE", value: "{" }, { type: "LEFT_BRACE", value: "{" },
{ type: "RIGHT_BRACE", value: "}" } { type: "RIGHT_BRACE", value: "}" },
]); ]);
expect(tokenize("{ }}")).toEqual([ expect(tokenize("{ }}")).toEqual([
{ type: "LEFT_BRACE", value: "{" }, { type: "LEFT_BRACE", value: "{" },
{ type: "RIGHT_BRACE", value: "}" }, { type: "RIGHT_BRACE", value: "}" },
{ type: "RIGHT_BRACE", value: "}" } { type: "RIGHT_BRACE", value: "}" },
]); ]);
expect(tokenize("a")).toEqual([{ type: "SYMBOL", value: "a" }]); expect(tokenize("a")).toEqual([{ type: "SYMBOL", value: "a" }]);
expect(tokenize("true")).toEqual([{ type: "SYMBOL", value: "true" }]); expect(tokenize("true")).toEqual([{ type: "SYMBOL", value: "true" }]);
@@ -25,15 +25,15 @@ describe("tokenizer", () => {
{ type: "SYMBOL", value: "a" }, { type: "SYMBOL", value: "a" },
{ type: "COLON", value: ":" }, { type: "COLON", value: ":" },
{ type: "VALUE", value: "2" }, { type: "VALUE", value: "2" },
{ type: "RIGHT_BRACE", value: "}" } { type: "RIGHT_BRACE", value: "}" },
]); ]);
expect(tokenize("a,")).toEqual([ expect(tokenize("a,")).toEqual([
{ type: "SYMBOL", value: "a" }, { type: "SYMBOL", value: "a" },
{ type: "COMMA", value: "," } { type: "COMMA", value: "," },
]); ]);
expect(tokenize("][")).toEqual([ expect(tokenize("][")).toEqual([
{ type: "RIGHT_BRACKET", value: "]" }, { type: "RIGHT_BRACKET", value: "]" },
{ type: "LEFT_BRACKET", value: "[" } { type: "LEFT_BRACKET", value: "[" },
]); ]);
}); });
@@ -44,23 +44,23 @@ describe("tokenizer", () => {
{ type: "OPERATOR", value: "<" }, { type: "OPERATOR", value: "<" },
{ type: "OPERATOR", value: ">" }, { type: "OPERATOR", value: ">" },
{ type: "OPERATOR", value: "!==" }, { type: "OPERATOR", value: "!==" },
{ type: "OPERATOR", value: "!=" } { type: "OPERATOR", value: "!=" },
]); ]);
expect(tokenize("typeof a")).toEqual([ expect(tokenize("typeof a")).toEqual([
{ type: "OPERATOR", value: "typeof " }, { type: "OPERATOR", value: "typeof " },
{ type: "SYMBOL", value: "a" } { type: "SYMBOL", value: "a" },
]); ]);
expect(tokenize("a...1")).toEqual([ expect(tokenize("a...1")).toEqual([
{ type: "SYMBOL", value: "a" }, { type: "SYMBOL", value: "a" },
{ type: "OPERATOR", value: "..." }, { type: "OPERATOR", value: "..." },
{ type: "VALUE", value: "1" } { type: "VALUE", value: "1" },
]); ]);
expect(tokenize("a in b")).toEqual([ expect(tokenize("a in b")).toEqual([
{ type: "SYMBOL", value: "a" }, { type: "SYMBOL", value: "a" },
{ type: "OPERATOR", value: "in " }, { type: "OPERATOR", value: "in " },
{ type: "SYMBOL", value: "b" } { type: "SYMBOL", value: "b" },
]); ]);
}); });
@@ -76,7 +76,7 @@ describe("tokenizer", () => {
expect(tokenize('"hello ged"')).toEqual([{ type: "VALUE", value: '"hello ged"' }]); expect(tokenize('"hello ged"')).toEqual([{ type: "VALUE", value: '"hello ged"' }]);
expect(tokenize('"hello ged"}')).toEqual([ expect(tokenize('"hello ged"}')).toEqual([
{ type: "VALUE", value: '"hello ged"' }, { type: "VALUE", value: '"hello ged"' },
{ type: "RIGHT_BRACE", value: "}" } { type: "RIGHT_BRACE", value: "}" },
]); ]);
expect(tokenize('"hello \\"ged\\""')).toEqual([{ type: "VALUE", value: '"hello \\"ged\\""' }]); expect(tokenize('"hello \\"ged\\""')).toEqual([{ type: "VALUE", value: '"hello \\"ged\\""' }]);
}); });
+10 -10
View File
@@ -7,16 +7,16 @@ exports[`Link component can render simple cases 1`] = `
let utils = this.constructor.utils; let utils = this.constructor.utils;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let _4 = utils.toObj({'router-link-active':scope['isActive']}); let _5 = utils.toObj({'router-link-active':scope['isActive']});
let _5 = scope['href']; let _6 = scope['href'];
let c6 = [], p6 = {key:6,attrs:{href: _5},class:_4,on:{}}; let c7 = [], p7 = {key:7,attrs:{href: _6},class:_5,on:{}};
let vn6 = h('a', p6, c6); let vn7 = h('a', p7, c7);
extra.handlers['click__7__'] = extra.handlers['click__7__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['navigate'](e);}; extra.handlers['click__8__'] = extra.handlers['click__8__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['navigate'](e);};
p6.on['click'] = extra.handlers['click__7__']; p7.on['click'] = extra.handlers['click__8__'];
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot9 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot8) { if (slot9) {
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c6, parent: extra.parent || context})); slot9.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context}));
} }
return vn6; return vn7;
}" }"
`; `;
+3 -3
View File
@@ -37,7 +37,7 @@ describe("Link component", () => {
const routes = [ const routes = [
{ name: "about", path: "/about" }, { name: "about", path: "/about" },
{ name: "users", path: "/users" } { name: "users", path: "/users" },
]; ];
router = new TestRouter(env, routes, { mode: "history" }); router = new TestRouter(env, routes, { mode: "history" });
@@ -71,7 +71,7 @@ describe("Link component", () => {
const routes = [ const routes = [
{ name: "about", path: "/about" }, { name: "about", path: "/about" },
{ name: "users", path: "/users" } { name: "users", path: "/users" },
]; ];
router = new TestRouter(env, routes, { mode: "history" }); router = new TestRouter(env, routes, { mode: "history" });
@@ -81,7 +81,7 @@ describe("Link component", () => {
expect(window.location.pathname).toBe("/users"); expect(window.location.pathname).toBe("/users");
var evt = new MouseEvent("click", { var evt = new MouseEvent("click", {
button: 1 button: 1,
}); });
fixture.querySelector("a")!.dispatchEvent(evt); fixture.querySelector("a")!.dispatchEvent(evt);
+1 -1
View File
@@ -41,7 +41,7 @@ describe("RouteComponent", () => {
const routes = [ const routes = [
{ name: "about", path: "/about", component: About }, { name: "about", path: "/about", component: About },
{ name: "users", path: "/users", component: Users } { name: "users", path: "/users", component: Users },
]; ];
router = new TestRouter(env, routes, { mode: "history" }); router = new TestRouter(env, routes, { mode: "history" });
+10 -10
View File
@@ -21,7 +21,7 @@ describe("router miscellaneous", () => {
test("validate routes shape", () => { test("validate routes shape", () => {
expect(() => { expect(() => {
router = new TestRouter(env, [ router = new TestRouter(env, [
{ name: "someroute", path: "/some/path", redirect: { abc: "hey" } as Destination } { name: "someroute", path: "/some/path", redirect: { abc: "hey" } as Destination },
]); ]);
}).toThrow(`Invalid destination: {"abc":"hey"}`); }).toThrow(`Invalid destination: {"abc":"hey"}`);
}); });
@@ -133,7 +133,7 @@ describe("getRouteParams", () => {
test("match some parameterized routes", () => { test("match some parameterized routes", () => {
router = new TestRouter(env, []); router = new TestRouter(env, []);
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "/invoices/3")).toEqual({ expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "/invoices/3")).toEqual({
id: "3" id: "3",
}); });
}); });
@@ -141,7 +141,7 @@ describe("getRouteParams", () => {
router = new TestRouter(env, [], { mode: "hash" }); router = new TestRouter(env, [], { mode: "hash" });
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "#/invoices/3")).toEqual( expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "#/invoices/3")).toEqual(
{ {
id: "3" id: "3",
} }
); );
}); });
@@ -151,7 +151,7 @@ describe("getRouteParams", () => {
expect( expect(
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "/invoices/3") router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "/invoices/3")
).toEqual({ ).toEqual({
id: 3 id: 3,
}); });
}); });
@@ -160,7 +160,7 @@ describe("getRouteParams", () => {
expect( expect(
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "#/invoices/3") router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "#/invoices/3")
).toEqual({ ).toEqual({
id: 3 id: 3,
}); });
}); });
}); });
@@ -169,7 +169,7 @@ describe("redirect", () => {
test("can redirect to other route", async () => { test("can redirect to other route", async () => {
router = new TestRouter(env, [ router = new TestRouter(env, [
{ name: "routea", path: "/some/path" }, { name: "routea", path: "/some/path" },
{ name: "routeb", path: "/some/other/path", redirect: { to: "routea" } } { name: "routeb", path: "/some/other/path", redirect: { to: "routea" } },
]); ]);
await router.start(); await router.start();
@@ -182,7 +182,7 @@ describe("redirect", () => {
test("can redirect to other path", async () => { test("can redirect to other path", async () => {
router = new TestRouter(env, [ router = new TestRouter(env, [
{ name: "routea", path: "/some/path" }, { name: "routea", path: "/some/path" },
{ name: "routeb", path: "/some/other/path", redirect: { path: "/some/path" } } { name: "routeb", path: "/some/other/path", redirect: { path: "/some/path" } },
]); ]);
router.navigate = jest.fn(router.navigate); router.navigate = jest.fn(router.navigate);
@@ -214,7 +214,7 @@ describe("beforeRouteEnter", () => {
const guard = jest.fn(() => false); const guard = jest.fn(() => false);
router = new TestRouter(env, [ router = new TestRouter(env, [
{ name: "routea", path: "/some/patha" }, { name: "routea", path: "/some/patha" },
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard } { name: "routeb", path: "/some/pathb", beforeRouteEnter: guard },
]); ]);
await router.start(); await router.start();
@@ -235,7 +235,7 @@ describe("beforeRouteEnter", () => {
router = new TestRouter(env, [ router = new TestRouter(env, [
{ name: "routea", path: "/some/patha" }, { name: "routea", path: "/some/patha" },
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard }, { name: "routeb", path: "/some/pathb", beforeRouteEnter: guard },
{ name: "routec", path: "/some/pathc" } { name: "routec", path: "/some/pathc" },
]); ]);
await router.start(); await router.start();
@@ -255,7 +255,7 @@ describe("beforeRouteEnter", () => {
}); });
router = new TestRouter(env, [ router = new TestRouter(env, [
{ name: "landing", path: "/", beforeRouteEnter: guard }, { name: "landing", path: "/", beforeRouteEnter: guard },
{ name: "otherroute", path: "/some/other/route" } { name: "otherroute", path: "/some/other/route" },
]); ]);
expect(window.location.pathname).toBe("/"); expect(window.location.pathname).toBe("/");
+28 -28
View File
@@ -8,7 +8,7 @@ describe("basic use", () => {
const actions = { const actions = {
inc({ state }, delta) { inc({ state }, delta) {
state.n += delta; state.n += delta;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -25,7 +25,7 @@ describe("basic use", () => {
state.n1 += delta1; state.n1 += delta1;
state.n2 += delta2; state.n2 += delta2;
state.n3 += delta3; state.n3 += delta3;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -47,7 +47,7 @@ describe("basic use", () => {
}, },
inc100({ dispatch }) { inc100({ dispatch }) {
dispatch("inc", 100); dispatch("inc", 100);
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -61,7 +61,7 @@ describe("basic use", () => {
const actions = { const actions = {
inc({ state }) { inc({ state }) {
return ++state.n; return ++state.n;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -87,7 +87,7 @@ describe("basic use", () => {
async setTo10({ dispatch }) { async setTo10({ dispatch }) {
await Promise.resolve(); await Promise.resolve();
dispatch("setN", 10); dispatch("setN", 10);
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -115,7 +115,7 @@ describe("basic use", () => {
await Promise.resolve(); await Promise.resolve();
dispatch("setN", 10); dispatch("setN", 10);
return 5; return 5;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -132,7 +132,7 @@ describe("basic use", () => {
const actions = { const actions = {
someaction({ env }) { someaction({ env }) {
expect(env).toBe(someEnv); expect(env).toBe(someEnv);
} },
}; };
const store = new Store({ state: {}, actions, env: someEnv }); const store = new Store({ state: {}, actions, env: someEnv });
@@ -145,15 +145,15 @@ describe("basic use", () => {
1: { 1: {
id: 1, id: 1,
name: "bertinchamps", name: "bertinchamps",
tasterID: 1 tasterID: 1,
} },
}, },
tasters: { tasters: {
1: { 1: {
id: 1, id: 1,
name: "aaron" name: "aaron",
} },
} },
}; };
const getters: { [key: string]: Getter } = { const getters: { [key: string]: Getter } = {
beerTasterName({ state }, beerID) { beerTasterName({ state }, beerID) {
@@ -161,7 +161,7 @@ describe("basic use", () => {
}, },
bestBeerName({ state }) { bestBeerName({ state }) {
return state.beers[1].name; return state.beers[1].name;
} },
}; };
const store = new Store({ state, actions: {}, getters }); const store = new Store({ state, actions: {}, getters });
expect(store.getters).toBeDefined(); expect(store.getters).toBeDefined();
@@ -176,15 +176,15 @@ describe("basic use", () => {
1: { 1: {
id: 1, id: 1,
name: "bertinchamps", name: "bertinchamps",
tasterID: 1 tasterID: 1,
} },
}, },
tasters: { tasters: {
1: { 1: {
id: 1, id: 1,
name: "aaron" name: "aaron",
} },
} },
}; };
const getters = { const getters = {
beerTasterName({ state }, beerID) { beerTasterName({ state }, beerID) {
@@ -192,14 +192,14 @@ describe("basic use", () => {
}, },
bestBeerName({ state }) { bestBeerName({ state }) {
return state.beers[1].name; return state.beers[1].name;
} },
}; };
const actions = { const actions = {
action({ getters }) { action({ getters }) {
expect(getters).toBeDefined(); expect(getters).toBeDefined();
expect(getters.bestBeerName()).toBe("bertinchamps"); expect(getters.bestBeerName()).toBe("bertinchamps");
expect(getters.beerTasterName(1)).toBe("aaron"); expect(getters.beerTasterName(1)).toBe("aaron");
} },
}; };
const store = new Store({ state, actions, getters }); const store = new Store({ state, actions, getters });
store.dispatch("action"); store.dispatch("action");
@@ -215,7 +215,7 @@ describe("basic use", () => {
}, },
c({}, i) { c({}, i) {
return `c${i}`; return `c${i}`;
} },
}; };
const store = new Store({ getters, state: {}, actions: {} }); const store = new Store({ getters, state: {}, actions: {} });
@@ -226,7 +226,7 @@ describe("basic use", () => {
describe("advanced state properties", () => { describe("advanced state properties", () => {
test("state in the store is reference equal after mutation", async () => { test("state in the store is reference equal after mutation", async () => {
const actions = { const actions = {
donothing() {} donothing() {},
}; };
const store = new Store({ state: {}, actions }); const store = new Store({ state: {}, actions });
const state = store.state; const state = store.state;
@@ -242,7 +242,7 @@ describe("advanced state properties", () => {
expect(state.a.length).toBe(3); expect(state.a.length).toBe(3);
const l = state.a.push(53); const l = state.a.push(53);
expect(l).toBe(4); expect(l).toBe(4);
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
store.dispatch("m"); store.dispatch("m");
@@ -253,11 +253,11 @@ describe("advanced state properties", () => {
const actions = { const actions = {
dosomething({ state }) { dosomething({ state }) {
Object.assign(state.westmalle, { a: 3, b: 4 }); Object.assign(state.westmalle, { a: 3, b: 4 });
} },
}; };
const store = new Store({ const store = new Store({
state: { westmalle: { a: 1, b: 2 } }, state: { westmalle: { a: 1, b: 2 } },
actions actions,
}); });
store.dispatch("dosomething"); store.dispatch("dosomething");
expect(store.state.westmalle).toEqual({ a: 3, b: 4 }); expect(store.state.westmalle).toEqual({ a: 3, b: 4 });
@@ -267,7 +267,7 @@ describe("advanced state properties", () => {
const actions = { const actions = {
inc({ state }) { inc({ state }) {
state.counter++; state.counter++;
} },
}; };
const state = { counter: 0 }; const state = { counter: 0 };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -284,7 +284,7 @@ describe("updates triggered by the store", () => {
const actions = { const actions = {
inc({ state }, delta) { inc({ state }, delta) {
state.n += delta; state.n += delta;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
store.on("update", null, () => updateCounter++); store.on("update", null, () => updateCounter++);
@@ -308,7 +308,7 @@ describe("updates triggered by the store", () => {
noop2({ state }) { noop2({ state }) {
const val = state.n; const val = state.n;
state.n = val; state.n = val;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
store.on("update", null, () => updateCounter++); store.on("update", null, () => updateCounter++);
+90 -90
View File
@@ -25,7 +25,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
addTodo({ state }, msg) { addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ }); state.todos.push({ msg, id: nextId++ });
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -34,7 +34,7 @@ describe("connecting a component to store", () => {
<div> <div>
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span> <span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
</div>`; </div>`;
todos = useStore(state => state.todos); todos = useStore((state) => state.todos);
} }
(<any>env).store = store; (<any>env).store = store;
@@ -56,7 +56,7 @@ describe("connecting a component to store", () => {
}, },
setNotNull({ state }) { setNotNull({ state }) {
state.nullValue = "ok"; state.nullValue = "ok";
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -70,15 +70,15 @@ describe("connecting a component to store", () => {
nullValue: string; nullValue: string;
constructor() { constructor() {
super(); super();
this.isBoolean = useStore(state => state.isBoolean, { this.isBoolean = useStore((state) => state.isBoolean, {
onUpdate: isBoolean => { onUpdate: (isBoolean) => {
this.isBoolean = isBoolean; this.isBoolean = isBoolean;
} },
}); });
this.nullValue = useStore(state => state.nullValue, { this.nullValue = useStore((state) => state.nullValue, {
onUpdate: nullValue => { onUpdate: (nullValue) => {
this.nullValue = nullValue; this.nullValue = nullValue;
} },
}); });
} }
} }
@@ -105,15 +105,15 @@ describe("connecting a component to store", () => {
class App extends Component { class App extends Component {
static template = xml`<div t-esc="mapAdd"/>`; static template = xml`<div t-esc="mapAdd"/>`;
storeProps = { storeProps = {
array: useStore(state => { array: useStore((state) => {
if (state.useSmallArray) { if (state.useSmallArray) {
return state.smallerArray; return state.smallerArray;
} }
return state.biggerArray; return state.biggerArray;
}) }),
}; };
get mapAdd() { get mapAdd() {
return this.storeProps.array.map(a => { return this.storeProps.array.map((a) => {
return a + 1; return a + 1;
}); });
} }
@@ -133,7 +133,7 @@ describe("connecting a component to store", () => {
test("throw error if no store is found", async () => { test("throw error if no store is found", async () => {
class App extends Component { class App extends Component {
static template = xml`<div></div>`; static template = xml`<div></div>`;
todos = useStore(state => state.todos); todos = useStore((state) => state.todos);
} }
let error; let error;
@@ -153,7 +153,7 @@ describe("connecting a component to store", () => {
class App extends Component { class App extends Component {
static template = xml`<div/>`; static template = xml`<div/>`;
storeState = useStore(state => state.a); storeState = useStore((state) => state.a);
} }
(<any>env).store = store; (<any>env).store = store;
@@ -170,7 +170,7 @@ describe("connecting a component to store", () => {
doSomething({ state }) { doSomething({ state }) {
state.a = 2; state.a = 2;
state.b = 3; state.b = 3;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -180,8 +180,8 @@ describe("connecting a component to store", () => {
<span t-esc="a.value"/> <span t-esc="a.value"/>
<span t-esc="b.value"/> <span t-esc="b.value"/>
</div>`; </div>`;
a = useStore(state => ({ value: state.a })); a = useStore((state) => ({ value: state.a }));
b = useStore(state => ({ value: state.b })); b = useStore((state) => ({ value: state.b }));
} }
App.prototype.__render = jest.fn(App.prototype.__render); App.prototype.__render = jest.fn(App.prototype.__render);
@@ -204,7 +204,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
addTodo({ state }, msg) { addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ }); state.todos.push({ msg, id: nextId++ });
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -213,7 +213,7 @@ describe("connecting a component to store", () => {
<div> <div>
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span> <span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
</div>`; </div>`;
todos = useStore(state => state.todos); todos = useStore((state) => state.todos);
} }
App.prototype.__render = jest.fn(App.prototype.__render); App.prototype.__render = jest.fn(App.prototype.__render);
@@ -241,13 +241,13 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
addTodo({ state }, msg) { addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ }); state.todos.push({ msg, id: nextId++ });
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
class App extends Component { class App extends Component {
static template = xml`<div><t t-esc="nbrTodos.value"/></div>`; static template = xml`<div><t t-esc="nbrTodos.value"/></div>`;
nbrTodos = useStore(state => ({ value: state.todos.length })); nbrTodos = useStore((state) => ({ value: state.todos.length }));
} }
(<any>env).store = store; (<any>env).store = store;
@@ -270,12 +270,12 @@ describe("connecting a component to store", () => {
}, },
incrementA({ state }) { incrementA({ state }) {
state.a++; state.a++;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
class App extends Component { class App extends Component {
static template = xml`<div><t t-esc="nbrTodos.value"/></div>`; static template = xml`<div><t t-esc="nbrTodos.value"/></div>`;
nbrTodos = useStore(state => ({ value: state.todos.length }), { isEqual: shallowEqual }); nbrTodos = useStore((state) => ({ value: state.todos.length }), { isEqual: shallowEqual });
} }
App.prototype.__render = jest.fn(App.prototype.__render); App.prototype.__render = jest.fn(App.prototype.__render);
@@ -303,7 +303,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
addTodo({ state }, msg) { addTodo({ state }, msg) {
state.todos.push({ msg, id: nextId++ }); state.todos.push({ msg, id: nextId++ });
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -312,7 +312,7 @@ describe("connecting a component to store", () => {
<div> <div>
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span> <span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
</div>`; </div>`;
todos = useStore(state => state.todos, { store }); todos = useStore((state) => state.todos, { store });
} }
const app = new App(); const app = new App();
@@ -331,8 +331,8 @@ describe("connecting a component to store", () => {
actions: { actions: {
inc({ state }) { inc({ state }) {
state.value++; state.value++;
} },
} },
}); });
(<any>env).store = store; (<any>env).store = store;
@@ -342,7 +342,7 @@ describe("connecting a component to store", () => {
<button t-on-click="dispatch('inc')">Inc</button> <button t-on-click="dispatch('inc')">Inc</button>
<span><t t-esc="storeState.value"/></span> <span><t t-esc="storeState.value"/></span>
</div>`; </div>`;
storeState = useStore(state => state); storeState = useStore((state) => state);
dispatch = useDispatch(); dispatch = useDispatch();
} }
const app = new App(); const app = new App();
@@ -360,15 +360,15 @@ describe("connecting a component to store", () => {
const state = { const state = {
todos: [ todos: [
{ id: 1, text: "jupiler" }, { id: 1, text: "jupiler" },
{ id: 2, text: "chimay" } { id: 2, text: "chimay" },
] ],
}; };
const store = new Store({ state, actions: {} }); const store = new Store({ state, actions: {} });
class TodoItem extends Component { class TodoItem extends Component {
static template = xml`<span><t t-esc="todo.text"/></span>`; static template = xml`<span><t t-esc="todo.text"/></span>`;
todo = useStore((state, props) => { todo = useStore((state, props) => {
return state.todos.find(t => t.id === props.todoId); return state.todos.find((t) => t.id === props.todoId);
}); });
} }
@@ -395,14 +395,14 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
addTodo({ state }, text) { addTodo({ state }, text) {
state.todos.push({ text, id: nextId++ }); state.todos.push({ text, id: nextId++ });
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
class TodoItem extends Component { class TodoItem extends Component {
static template = xml`<span><t t-esc="todo.text"/></span>`; static template = xml`<span><t t-esc="todo.text"/></span>`;
todo = useStore((state, props) => { todo = useStore((state, props) => {
return state.todos.find(t => t.id === props.id); return state.todos.find((t) => t.id === props.id);
}); });
} }
@@ -412,7 +412,7 @@ describe("connecting a component to store", () => {
<TodoItem t-foreach="todos" t-as="todo" id="todo.id" t-key="todo.id"/> <TodoItem t-foreach="todos" t-as="todo" id="todo.id" t-key="todo.id"/>
</div>`; </div>`;
static components = { TodoItem }; static components = { TodoItem };
todos = useStore(state => state.todos); todos = useStore((state) => state.todos);
} }
(<any>env).store = store; (<any>env).store = store;
@@ -431,16 +431,16 @@ describe("connecting a component to store", () => {
importantID: 1, importantID: 1,
todos: [ todos: [
{ id: 1, text: "jupiler" }, { id: 1, text: "jupiler" },
{ id: 2, text: "bertinchamps" } { id: 2, text: "bertinchamps" },
] ],
}; };
const getters = { const getters = {
importantTodoText({ state }) { importantTodoText({ state }) {
return state.todos.find(todo => todo.id === state.importantID).text; return state.todos.find((todo) => todo.id === state.importantID).text;
}, },
text({ state }, id) { text({ state }, id) {
return state.todos.find(todo => todo.id === id).text; return state.todos.find((todo) => todo.id === id).text;
} },
}; };
const store = new Store({ state, getters }); const store = new Store({ state, getters });
@@ -452,10 +452,10 @@ describe("connecting a component to store", () => {
</div>`; </div>`;
getters = useGetters(); getters = useGetters();
storeProps = useStore((state, props) => { storeProps = useStore((state, props) => {
const todo = state.todos.find(t => t.id === props.id); const todo = state.todos.find((t) => t.id === props.id);
return { return {
activeTodoText: this.getters.text(todo.id), activeTodoText: this.getters.text(todo.id),
importantTodoText: this.getters.importantTodoText() importantTodoText: this.getters.importantTodoText(),
}; };
}); });
} }
@@ -468,7 +468,7 @@ describe("connecting a component to store", () => {
<TodoItem id="todo.id" t-key="todo.id"/> <TodoItem id="todo.id" t-key="todo.id"/>
</t> </t>
</div>`; </div>`;
todos = useStore(state => state.todos); todos = useStore((state) => state.todos);
} }
(<any>env).store = store; (<any>env).store = store;
@@ -513,13 +513,13 @@ describe("connecting a component to store", () => {
</div>`; </div>`;
// we have here a new object // we have here a new object
data = useStore(state => ({ beers: state.beers, otherKey: 1 })); data = useStore((state) => ({ beers: state.beers, otherKey: 1 }));
} }
const actions = { const actions = {
addBeer({ state }, name) { addBeer({ state }, name) {
state.beers.push({ name }); state.beers.push({ name });
} },
}; };
const state = { beers: [{ name: "jupiler" }] }; const state = { beers: [{ name: "jupiler" }] };
@@ -542,9 +542,9 @@ describe("connecting a component to store", () => {
class Beer extends Component { class Beer extends Component {
static template = xml`<span><t t-esc="beer.name"/></span>`; static template = xml`<span><t t-esc="beer.name"/></span>`;
beer = useStore((state, props) => state.beers[props.id], { beer = useStore((state, props) => state.beers[props.id], {
onUpdate: result => { onUpdate: (result) => {
++counter; ++counter;
} },
}); });
} }
@@ -558,7 +558,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
renameBeer({ state }, { id, name }) { renameBeer({ state }, { id, name }) {
state.beers[id].name = name; state.beers[id].name = name;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
(<any>env).store = store; (<any>env).store = store;
@@ -609,7 +609,7 @@ describe("connecting a component to store", () => {
data = useStore((state, props) => ({ data = useStore((state, props) => ({
selected: state.beers[props.id], selected: state.beers[props.id],
consumed: state.beers[state.consumedID] || null, consumed: state.beers[state.consumedID] || null,
taster: state.taster taster: state.taster,
})); }));
} }
@@ -622,14 +622,14 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
consume({ state }, beerId) { consume({ state }, beerId) {
state.consumedID = beerId; state.consumedID = beerId;
} },
}; };
const state = { const state = {
beers: { beers: {
1: { name: "jupiler" } 1: { name: "jupiler" },
}, },
consumedID: null, consumedID: null,
taster: "aaron" taster: "aaron",
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
(<any>env).store = store; (<any>env).store = store;
@@ -665,11 +665,11 @@ describe("connecting a component to store", () => {
<span t-if="info.selected">selected:<t t-esc="info.selected.name"/></span> <span t-if="info.selected">selected:<t t-esc="info.selected.name"/></span>
<span t-if="info.consumed">consumed:<t t-esc="info.consumed.name"/></span> <span t-if="info.consumed">consumed:<t t-esc="info.consumed.name"/></span>
</div>`; </div>`;
info = useStore(function(state, props) { info = useStore(function (state, props) {
return { return {
selected: state.beers[props.id], selected: state.beers[props.id],
consumed: state.beers[state.consumedID] || null, consumed: state.beers[state.consumedID] || null,
taster: state.taster taster: state.taster,
}; };
}); });
} }
@@ -689,14 +689,14 @@ describe("connecting a component to store", () => {
}, },
renameBeer({ state }, { beerId, name }) { renameBeer({ state }, { beerId, name }) {
state.beers[beerId].name = name; state.beers[beerId].name = name;
} },
}; };
const state = { const state = {
beers: { beers: {
1: { name: "jupiler" } 1: { name: "jupiler" },
}, },
consumedID: null, consumedID: null,
taster: "aaron" taster: "aaron",
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
(<any>env).store = store; (<any>env).store = store;
@@ -747,11 +747,11 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
setValue({ state }, val) { setValue({ state }, val) {
state.x.val = val; state.x.val = val;
} },
}; };
class Parent extends Component { class Parent extends Component {
static template = xml`<div><t t-esc="x.val"/></div>`; static template = xml`<div><t t-esc="x.val"/></div>`;
x = useStore(state => { x = useStore((state) => {
return Object.assign({}, state.x); return Object.assign({}, state.x);
}); });
dispatch = useDispatch(); dispatch = useDispatch();
@@ -782,11 +782,11 @@ describe("connecting a component to store", () => {
static template = xml`<div><Child key="state.current"/></div>`; static template = xml`<div><Child key="state.current"/></div>`;
static components = { Child }; static components = { Child };
state = useStore(state => { state = useStore((state) => {
steps.push("parent"); steps.push("parent");
return { return {
current: state.current, current: state.current,
isvisible: state.isvisible isvisible: state.isvisible,
}; };
}); });
} }
@@ -795,7 +795,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
setCurrent({ state }, c) { setCurrent({ state }, c) {
state.current = c; state.current = c;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -831,7 +831,7 @@ describe("connecting a component to store", () => {
<Child t-if="state.flag" someId="state.someId"/> <Child t-if="state.flag" someId="state.someId"/>
</div>`; </div>`;
static components = { Child }; static components = { Child };
state = useStore(s => { state = useStore((s) => {
steps.push("parent"); steps.push("parent");
return { flag: s.flag, someId: s.someId }; return { flag: s.flag, someId: s.someId };
}); });
@@ -846,7 +846,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
setFlagToFalse({ state }) { setFlagToFalse({ state }) {
state.flag = false; state.flag = false;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -874,15 +874,15 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
editTodo({ state }) { editTodo({ state }) {
state.todos[1].title = "abc"; state.todos[1].title = "abc";
} },
}; };
const todos = { 1: { id: 1, title: "kikoou" } }; const todos = { 1: { id: 1, title: "kikoou" } };
const state = { const state = {
todos todos,
}; };
const store = new Store({ const store = new Store({
state, state,
actions actions,
}); });
class TodoItem extends Component<{}, EnvWithStore> { class TodoItem extends Component<{}, EnvWithStore> {
@@ -894,7 +894,7 @@ describe("connecting a component to store", () => {
state = useStore((state, props) => { state = useStore((state, props) => {
steps.push("item:usestore"); steps.push("item:usestore");
return { return {
todo: state.todos[props.id] todo: state.todos[props.id],
}; };
}); });
@@ -914,7 +914,7 @@ describe("connecting a component to store", () => {
</t> </t>
</div>`; </div>`;
static components = { TodoItem }; static components = { TodoItem };
state = useStore(state => { state = useStore((state) => {
steps.push("app:usestore"); steps.push("app:usestore");
return { todos: state.todos }; return { todos: state.todos };
}); });
@@ -947,15 +947,15 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
removeTodo({ state }) { removeTodo({ state }) {
delete state.todos[1]; delete state.todos[1];
} },
}; };
const todos = { 1: { id: 1, title: "kikoou" } }; const todos = { 1: { id: 1, title: "kikoou" } };
const state = { const state = {
todos todos,
}; };
const store = new Store({ const store = new Store({
state, state,
actions actions,
}); });
class TodoItem extends Component<{}, EnvWithStore> { class TodoItem extends Component<{}, EnvWithStore> {
@@ -967,7 +967,7 @@ describe("connecting a component to store", () => {
state = useStore((state, props) => { state = useStore((state, props) => {
steps.push("item:usestore"); steps.push("item:usestore");
return { return {
todo: state.todos[props.id] todo: state.todos[props.id],
}; };
}); });
@@ -988,7 +988,7 @@ describe("connecting a component to store", () => {
</t> </t>
</div>`; </div>`;
static components = { TodoItem }; static components = { TodoItem };
state = useStore(state => { state = useStore((state) => {
steps.push("app:usestore"); steps.push("app:usestore");
return { todos: state.todos }; return { todos: state.todos };
}); });
@@ -1016,7 +1016,7 @@ describe("connecting a component to store", () => {
"item:usestore", "item:usestore",
"item:render", "item:render",
"app:usestore", "app:usestore",
"app:render" "app:render",
]); ]);
expect(fixture.innerHTML).toBe('<div class="todoapp"></div>'); expect(fixture.innerHTML).toBe('<div class="todoapp"></div>');
}); });
@@ -1026,7 +1026,7 @@ describe("connecting a component to store", () => {
class App extends Component { class App extends Component {
static template = xml`<div><t t-esc="store.msg"/></div>`; static template = xml`<div><t t-esc="store.msg"/></div>`;
store = useStore(s => ({ msg: s.msg })); store = useStore((s) => ({ msg: s.msg }));
willPatch() { willPatch() {
steps.push("willpatch"); steps.push("willpatch");
@@ -1040,7 +1040,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
setMsg({ state }, c) { setMsg({ state }, c) {
state.msg = c; state.msg = c;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -1061,7 +1061,7 @@ describe("connecting a component to store", () => {
class Child extends Component { class Child extends Component {
static template = xml`<div><t t-esc="store.val"/></div>`; static template = xml`<div><t t-esc="store.val"/></div>`;
store = useStore(s => s); store = useStore((s) => s);
} }
class Parent extends Component { class Parent extends Component {
@@ -1099,7 +1099,7 @@ describe("connecting a component to store", () => {
class Child extends Component { class Child extends Component {
static template = xml`<div><t t-esc="store.val"/></div>`; static template = xml`<div><t t-esc="store.val"/></div>`;
store = useStore(s => { store = useStore((s) => {
steps.push("child selector"); steps.push("child selector");
return s; return s;
}); });
@@ -1107,7 +1107,7 @@ describe("connecting a component to store", () => {
class Parent extends Component { class Parent extends Component {
static template = xml`<div><Child t-if="store.child" /></div>`; static template = xml`<div><Child t-if="store.child" /></div>`;
static components = { Child }; static components = { Child };
store = useStore(s => { store = useStore((s) => {
steps.push("parent selector"); steps.push("parent selector");
return s; return s;
}); });
@@ -1118,7 +1118,7 @@ describe("connecting a component to store", () => {
const actions = { const actions = {
toggleChild({ state }) { toggleChild({ state }) {
state.child = !state.child; state.child = !state.child;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
(<any>env).store = store; (<any>env).store = store;
@@ -1137,18 +1137,18 @@ describe("connecting a component to store", () => {
test("dispatch an action", async () => { test("dispatch an action", async () => {
class App extends Component { class App extends Component {
static template = xml`<div><t t-esc="store.counter"/></div>`; static template = xml`<div><t t-esc="store.counter"/></div>`;
store = useStore(state => state); store = useStore((state) => state);
dispatch = useDispatch(); dispatch = useDispatch();
} }
const state = { const state = {
counter: 0 counter: 0,
}; };
const actions = { const actions = {
inc({ state }) { inc({ state }) {
return ++state.counter; return ++state.counter;
} },
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -1185,21 +1185,21 @@ describe("various scenarios", () => {
await Promise.resolve(); await Promise.resolve();
delete state.attachments[100]; delete state.attachments[100];
state.messages[10].attachmentIds = []; state.messages[10].attachmentIds = [];
} },
}; };
const state = { const state = {
attachments: { attachments: {
100: { 100: {
id: 100, id: 100,
name: "text.txt" name: "text.txt",
} },
}, },
messages: { messages: {
10: { 10: {
attachmentIds: [100], attachmentIds: [100],
id: 10 id: 10,
} },
} },
}; };
const store = new Store({ actions, state }); const store = new Store({ actions, state });
@@ -1220,7 +1220,7 @@ describe("various scenarios", () => {
<Attachment t-foreach="store.attachmentIds" t-key="attachmentId" t-as="attachmentId" id="attachmentId"/> <Attachment t-foreach="store.attachmentIds" t-key="attachmentId" t-as="attachmentId" id="attachmentId"/>
</div>`; </div>`;
store = useStore(state => ({ attachmentIds: state.messages[10].attachmentIds })); store = useStore((state) => ({ attachmentIds: state.messages[10].attachmentIds }));
static components = { Attachment }; static components = { Attachment };
state = { isAttachmentDeleted: false }; state = { isAttachmentDeleted: false };
dispatch = useDispatch(); dispatch = useDispatch();
+2 -2
View File
@@ -20,7 +20,7 @@ debugOwl(owl, {});
test("can log full lifecycle", async () => { test("can log full lifecycle", async () => {
const steps: string[] = []; const steps: string[] = [];
const log = console.log; const log = console.log;
console.log = arg => steps.push(arg); console.log = (arg) => steps.push(arg);
class Child extends Component { class Child extends Component {
static template = xml`<div>child</div>`; static template = xml`<div>child</div>`;
@@ -51,7 +51,7 @@ test("can log full lifecycle", async () => {
"[OWL_DEBUG] Child<id=2> rendering template", "[OWL_DEBUG] Child<id=2> rendering template",
"[OWL_DEBUG] Parent<id=1> willPatch", "[OWL_DEBUG] Parent<id=1> willPatch",
"[OWL_DEBUG] Child<id=2> mounted", "[OWL_DEBUG] Child<id=2> mounted",
"[OWL_DEBUG] Parent<id=1> patched" "[OWL_DEBUG] Parent<id=1> patched",
]); ]);
console.log = log; console.log = log;
}); });
+2 -2
View File
@@ -19,7 +19,7 @@ debugOwl(owl, { logScheduler: true });
test("can log scheduler start and stop", async () => { test("can log scheduler start and stop", async () => {
const steps: string[] = []; const steps: string[] = [];
const log = console.log; const log = console.log;
console.log = arg => steps.push(arg); console.log = (arg) => steps.push(arg);
class Child extends Component { class Child extends Component {
static template = xml`<div>child</div>`; static template = xml`<div>child</div>`;
@@ -44,7 +44,7 @@ test("can log scheduler start and stop", async () => {
"[OWL_DEBUG] Child<id=2> rendering template", "[OWL_DEBUG] Child<id=2> rendering template",
"[OWL_DEBUG] Child<id=2> mounted", "[OWL_DEBUG] Child<id=2> mounted",
"[OWL_DEBUG] Parent<id=1> mounted", "[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue" "[OWL_DEBUG] scheduler: stop running tasks queue",
]); ]);
console.log = log; console.log = log;
}); });
+2 -2
View File
@@ -19,7 +19,7 @@ debugOwl(owl, { logScheduler: true });
test("log a specific message for render method calls if component is not mounted", async () => { test("log a specific message for render method calls if component is not mounted", async () => {
const steps: string[] = []; const steps: string[] = [];
const log = console.log; const log = console.log;
console.log = arg => steps.push(arg); console.log = (arg) => steps.push(arg);
class Parent extends Component { class Parent extends Component {
static template = xml`<div><t t-esc="state.value"/></div>`; static template = xml`<div><t t-esc="state.value"/></div>`;
@@ -40,7 +40,7 @@ test("log a specific message for render method calls if component is not mounted
"[OWL_DEBUG] Parent<id=1> mounted", "[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue", "[OWL_DEBUG] scheduler: stop running tasks queue",
"[OWL_DEBUG] Parent<id=1> willUnmount", "[OWL_DEBUG] Parent<id=1> willUnmount",
"[OWL_DEBUG] Parent<id=1> render (warning: component is not mounted, this render has no effect)" "[OWL_DEBUG] Parent<id=1> render (warning: component is not mounted, this render has no effect)",
]); ]);
console.log = log; console.log = log;
}); });
+2 -2
View File
@@ -19,7 +19,7 @@ debugOwl(owl, { logScheduler: true });
test("log a sub component with non stringifiable props", async () => { test("log a sub component with non stringifiable props", async () => {
const steps: string[] = []; const steps: string[] = [];
const log = console.log; const log = console.log;
console.log = arg => steps.push(arg); console.log = (arg) => steps.push(arg);
class Child extends Component { class Child extends Component {
static template = xml`<span><t t-esc="props.obj.val"/></span>`; static template = xml`<span><t t-esc="props.obj.val"/></span>`;
@@ -51,7 +51,7 @@ test("log a sub component with non stringifiable props", async () => {
"[OWL_DEBUG] Parent<id=1> mounted", "[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue", "[OWL_DEBUG] scheduler: stop running tasks queue",
"[OWL_DEBUG] Parent<id=1> willUnmount", "[OWL_DEBUG] Parent<id=1> willUnmount",
"[OWL_DEBUG] Child<id=2> willUnmount" "[OWL_DEBUG] Child<id=2> willUnmount",
]); ]);
console.log = log; console.log = log;
}); });
+7 -7
View File
@@ -60,14 +60,14 @@ function getFiles(path: string[] = []): FileData[] {
if (path.length === 0) { if (path.length === 0) {
const baseFiles: FileData[] = [ const baseFiles: FileData[] = [
{ name: "README.md", path: [], links: [], sections: [], fullName: "README.md" }, { name: "README.md", path: [], links: [], sections: [], fullName: "README.md" },
{ name: "roadmap.md", path: [], links: [], sections: [], fullName: "roadmap.md" } { name: "roadmap.md", path: [], links: [], sections: [], fullName: "roadmap.md" },
]; ];
const rest = getFiles(["doc"]); const rest = getFiles(["doc"]);
const result = baseFiles.concat(rest); const result = baseFiles.concat(rest);
result.forEach(addMardownData); result.forEach(addMardownData);
return result; return result;
} }
const files = fs.readdirSync(path.join("/"), { withFileTypes: true }).map(f => { const files = fs.readdirSync(path.join("/"), { withFileTypes: true }).map((f) => {
if (f.isDirectory()) { if (f.isDirectory()) {
return getFiles(path.concat(f.name)); return getFiles(path.concat(f.name));
} }
@@ -78,8 +78,8 @@ function getFiles(path: string[] = []): FileData[] {
path, path,
links: [], links: [],
sections: [], sections: [],
fullName fullName,
} },
]; ];
}); });
return Array.prototype.concat(...files); return Array.prototype.concat(...files);
@@ -127,7 +127,7 @@ export function isLinkValid(link: MarkDownLink, current: FileData, files: FileDa
} }
// Step 4: check if there is a matching file // Step 4: check if there is a matching file
let target: FileData | undefined = files.find(f => f.fullName === linkFullName); let target: FileData | undefined = files.find((f) => f.fullName === linkFullName);
if (!target) { if (!target) {
return false; return false;
} }
@@ -135,7 +135,7 @@ export function isLinkValid(link: MarkDownLink, current: FileData, files: FileDa
// Step 5: if necessary, check if there is a corresponding link inside the target // Step 5: if necessary, check if there is a corresponding link inside the target
// link name // link name
if (hash) { if (hash) {
if (!target.sections.find(s => s.slug === hash)) { if (!target.sections.find((s) => s.slug === hash)) {
return false; return false;
} }
} }
@@ -153,7 +153,7 @@ function slugify(str) {
.toLowerCase() .toLowerCase()
.replace(/\//g, "") // remove / .replace(/\//g, "") // remove /
.replace(/\s+/g, "-") // Replace spaces with - .replace(/\s+/g, "-") // Replace spaces with -
.replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters .replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
.replace(/&/g, "-and-") // Replace & with and .replace(/&/g, "-and-") // Replace & with and
.replace(/[^\w\-]+/g, "") // Remove all non-word characters .replace(/[^\w\-]+/g, "") // Remove all non-word characters
.replace(/\-\-+/g, "-") // Replace multiple - with single - .replace(/\-\-+/g, "-") // Replace multiple - with single -
+199 -192
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,5 +1,6 @@
import { SAMPLES } from "./samples.js"; import { SAMPLES } from "./samples.js";
const { useState, useRef, onMounted, onWillUnmount } = owl.hooks; const { mount, hooks } = owl;
const { useState, useRef, onMounted, onWillUnmount } = hooks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Constants, helpers, utils // Constants, helpers, utils
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -419,9 +420,8 @@ async function start() {
owl.utils.whenReady() owl.utils.whenReady()
]); ]);
const qweb = new owl.QWeb({ templates }); const qweb = new owl.QWeb({ templates });
owl.Component.env = { qweb }; const env = { qweb };
const app = new App(); await mount(App, {target: document.body, env});
app.mount(document.body);
} }
start(); start();
+27 -38
View File
@@ -1,5 +1,5 @@
const COMPONENTS = `// In this example, we show how components can be defined and created. const COMPONENTS = `// In this example, we show how components can be defined and created.
const { Component, useState } = owl; const { Component, useState, mount } = owl;
class Greeter extends Component { class Greeter extends Component {
constructor() { constructor() {
@@ -22,8 +22,7 @@ class App extends Component {
App.components = { Greeter }; App.components = { Greeter };
// Application setup // Application setup
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const COMPONENTS_XML = `<templates> const COMPONENTS_XML = `<templates>
@@ -50,7 +49,7 @@ const COMPONENTS_CSS = `.greeter {
const ANIMATION = `// The goal of this component is to see how the t-transition directive can be const ANIMATION = `// The goal of this component is to see how the t-transition directive can be
// used to generate simple transition effects. // used to generate simple transition effects.
const { Component, useState } = owl; const { Component, useState, mount } = owl;
class Counter extends Component { class Counter extends Component {
constructor() { constructor() {
@@ -80,8 +79,7 @@ class App extends Component {
} }
App.components = { Counter }; App.components = { Counter };
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const ANIMATION_XML = `<templates> const ANIMATION_XML = `<templates>
@@ -193,7 +191,7 @@ const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks
// methods in the console. Try modifying its state by clicking on it, or by // methods in the console. Try modifying its state by clicking on it, or by
// clicking on the two main buttons, and look into the console to see what // clicking on the two main buttons, and look into the console to see what
// happens. // happens.
const { Component, useState } = owl; const { Component, useState, mount } = owl;
class DemoComponent extends Component { class DemoComponent extends Component {
constructor() { constructor() {
@@ -240,8 +238,7 @@ class App extends Component {
} }
App.components = { DemoComponent }; App.components = { DemoComponent };
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const LIFECYCLE_DEMO_XML = `<templates> const LIFECYCLE_DEMO_XML = `<templates>
@@ -273,7 +270,8 @@ const LIFECYCLE_CSS = `button {
}`; }`;
const HOOKS_DEMO = `// In this example, we show how hooks can be used or defined. const HOOKS_DEMO = `// In this example, we show how hooks can be used or defined.
const {useState, onMounted, onWillUnmount} = owl.hooks; const { hooks, mount } = owl;
const {useState, onMounted, onWillUnmount} = hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse // We define here a custom behaviour: this hook tracks the state of the mouse
// position // position
@@ -312,8 +310,7 @@ class App extends owl.Component {
} }
// Application setup // Application setup
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const HOOKS_DEMO_XML = `<templates> const HOOKS_DEMO_XML = `<templates>
@@ -332,7 +329,7 @@ const HOOKS_CSS = `button {
const CONTEXT_JS = `// In this example, we show how components can use the Context and 'useContext' const CONTEXT_JS = `// In this example, we show how components can use the Context and 'useContext'
// hook to share information between them. // hook to share information between them.
const { Component, Context } = owl; const { Component, Context, mount } = owl;
const { useContext } = owl.hooks; const { useContext } = owl.hooks;
class ToolbarButton extends Component { class ToolbarButton extends Component {
@@ -367,8 +364,7 @@ const themeContext = new Context({
}); });
// Add the themeContext the environment to make it available to all components // Add the themeContext the environment to make it available to all components
App.env.themeContext = themeContext; App.env.themeContext = themeContext;
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const CONTEXT_XML = `<templates> const CONTEXT_XML = `<templates>
@@ -395,7 +391,7 @@ const TODO_APP_STORE = `// This example is an implementation of the TodoList app
// //
// In this implementation, we use the owl Store class to manage the state. It // In this implementation, we use the owl Store class to manage the state. It
// is very similar to the VueX store. // is very similar to the VueX store.
const { Component, useState } = owl; const { Component, useState, mount } = owl;
const { useRef, useStore, useDispatch, onPatched, onMounted } = owl.hooks; const { useRef, useStore, useDispatch, onPatched, onMounted } = owl.hooks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -568,8 +564,7 @@ function makeStore() {
} }
TodoApp.env.store = makeStore(); TodoApp.env.store = makeStore();
const app = new TodoApp(); mount(TodoApp, { target: document.body });
app.mount(document.body);
`; `;
const TODO_APP_STORE_XML = `<templates> const TODO_APP_STORE_XML = `<templates>
@@ -1060,8 +1055,7 @@ function setupResponsivePlugin(env) {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
setupResponsivePlugin(App.env); setupResponsivePlugin(App.env);
const app = new App(); owl.mount(App, { target: document.body });
app.mount(document.body);
`; `;
const RESPONSIVE_XML = `<templates> const RESPONSIVE_XML = `<templates>
@@ -1173,7 +1167,7 @@ const SLOTS = `// We show here how slots can be used to create generic component
// //
// Note that the t-on-click event, defined in the App template, is executed in // Note that the t-on-click event, defined in the App template, is executed in
// the context of the App component, even though it is inside the Card component // the context of the App component, even though it is inside the Card component
const { Component, useState } = owl; const { Component, useState, mount } = owl;
class Card extends Component { class Card extends Component {
constructor() { constructor() {
@@ -1211,8 +1205,8 @@ class App extends Component {
App.components = {Card, Counter}; App.components = {Card, Counter};
// Application setup // Application setup
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);`; `;
const SLOTS_XML = `<templates> const SLOTS_XML = `<templates>
<div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'"> <div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'">
@@ -1298,7 +1292,7 @@ const ASYNC_COMPONENTS = `// This example will not work if your browser does not
// However, we don't want renderings of the other sub component to be delayed // However, we don't want renderings of the other sub component to be delayed
// because of the slow component. We use the AsyncRoot component for this // because of the slow component. We use the AsyncRoot component for this
// purpose. Try removing it to see the difference. // purpose. Try removing it to see the difference.
const { Component, useState } = owl; const { Component, useState, mount } = owl;
const { AsyncRoot } = owl.misc; const { AsyncRoot } = owl.misc;
class SlowComponent extends Component { class SlowComponent extends Component {
@@ -1329,8 +1323,7 @@ class App extends Component {
} }
App.components = {SlowComponent, NotificationList, AsyncRoot}; App.components = {SlowComponent, NotificationList, AsyncRoot};
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const ASYNC_COMPONENTS_XML = `<templates> const ASYNC_COMPONENTS_XML = `<templates>
@@ -1385,7 +1378,7 @@ const FORM = `// This example illustrate how the t-model directive can be used t
// data between html inputs (and select/textareas) and the state of a component. // data between html inputs (and select/textareas) and the state of a component.
// Note that there are two controls with t-model="color": they are totally // Note that there are two controls with t-model="color": they are totally
// synchronized. // synchronized.
const { Component, useState } = owl; const { Component, useState, mount } = owl;
class Form extends Component { class Form extends Component {
constructor() { constructor() {
@@ -1401,8 +1394,7 @@ class Form extends Component {
} }
// Application setup // Application setup
const form = new Form(); mount(Form, { target: document.body });
form.mount(document.body);
`; `;
const FORM_XML = `<templates> const FORM_XML = `<templates>
@@ -1448,7 +1440,7 @@ const PORTAL_COMPONENTS = `
// This shows the expected use case of Portal // This shows the expected use case of Portal
// which is to implement something similar // which is to implement something similar
// to bootstrap modal // to bootstrap modal
const { Component, useState } = owl; const { Component, useState, mount } = owl;
const { Portal } = owl.misc; const { Portal } = owl.misc;
class Modal extends Component {} class Modal extends Component {}
@@ -1470,8 +1462,7 @@ class App extends Component {
App.components = { Dialog , Interstellar }; App.components = { Dialog , Interstellar };
// Application setup // Application setup
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const PORTAL_XML = ` const PORTAL_XML = `
@@ -1559,7 +1550,7 @@ const WMS = `// This example is slightly more complex than usual. We demonstrate
// - minimal width/height // - minimal width/height
// - better heuristic for initial window position // - better heuristic for initial window position
// - ... // - ...
const { Component, useState } = owl; const { Component, useState, mount } = owl;
const { useRef } = owl.hooks; const { useRef } = owl.hooks;
class HelloWorld extends Component {} class HelloWorld extends Component {}
@@ -1699,8 +1690,7 @@ const windows = [
]; ];
App.env.windows = windows; App.env.windows = windows;
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const WMS_XML = `<templates> const WMS_XML = `<templates>
@@ -1818,7 +1808,7 @@ const SFC = `// This example illustrates how Owl enables single file components,
// Note that this example has no external xml or css file, everything is // Note that this example has no external xml or css file, everything is
// contained in a single js file. // contained in a single js file.
const { Component, useState, tags } = owl; const { Component, useState, tags, mount } = owl;
const { xml, css } = tags; const { xml, css } = tags;
// Counter component // Counter component
@@ -1850,8 +1840,7 @@ App.template = APP_TEMPLATE;
App.components = { Counter }; App.components = { Counter };
// Application setup // Application setup
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
export const SAMPLES = [ export const SAMPLES = [
+23 -58
View File
@@ -3,10 +3,9 @@ const readline = require("readline");
const fs = require("fs"); const fs = require("fs");
const exec = require("child_process").exec; const exec = require("child_process").exec;
const chalk = require("chalk"); const chalk = require("chalk");
const GitHub = require("github-api");
const REL_NOTES_FILE = `release-notes.md`; const REL_NOTES_FILE = `release-notes.md`;
const STEPS = 10; const STEPS = 8;
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
@@ -28,7 +27,8 @@ async function startRelease() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step 1/${STEPS}: collecting info...`); log(`Step 1/${STEPS}: collecting info...`);
const current = package.version; const current = package.version;
const next = await ask("Next version: "); let next = await ask("Next version: ");
if (next[0] === 'v') next = next.substring(1);
let file = await ask(`Release notes (${REL_NOTES_FILE}): `); let file = await ask(`Release notes (${REL_NOTES_FILE}): `);
file = file || REL_NOTES_FILE; file = file || REL_NOTES_FILE;
let content; let content;
@@ -39,11 +39,12 @@ async function startRelease() {
log("Cannot find release notes... Aborting"); log("Cannot find release notes... Aborting");
return; return;
} }
let shouldBeDraft = await ask(`Should be a draft [y/n] ? (n)`);
// Todo: add playground update feature let draft = ""
// let shouldUpdateStr = await ask("Update Playground? (y/n)"); if (shouldBeDraft.toLowerCase() === 'y')
// const shouldUpdatePlayground = shouldUpdateStr === "y"; {
const token = await ask("Github token: "); draft = "--draft";
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step 2/${STEPS}: running tests...`); log(`Step 2/${STEPS}: running tests...`);
@@ -52,14 +53,12 @@ async function startRelease() {
log("Test suite does not pass. Aborting."); log("Test suite does not pass. Aborting.");
return; return;
} }
await ask("Ready for next step...");
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step 3/${STEPS}: updating package.json, readme.md and roadmap.md...`); log(`Step 3/${STEPS}: updating package.json, readme.md and roadmap.md...`);
await replaceInFile("./package.json", current, next); await replaceInFile("./package.json", current, next);
await replaceInFile("./README.md", current, next); await replaceInFile("./README.md", current, next);
await replaceInFile("./roadmap.md", current, next); await replaceInFile("./roadmap.md", current, next);
await ask("Ready for next step...");
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step 4/${STEPS}: creating git commit...`); log(`Step 4/${STEPS}: creating git commit...`);
@@ -68,61 +67,40 @@ async function startRelease() {
log("Git commit failed. Aborting."); log("Git commit failed. Aborting.");
return; return;
} }
await ask("Ready for next step...");
// -------------------j-------------------------------------------------------- // ----------------------------------------------------------------------------
log(`Step 5/${STEPS}: building owl (iife version)...`); log(`Step 5/${STEPS}: building owl...`);
await execCommand("npm run prettier");
const buildResult = await execCommand("npm run build"); const buildResult = await execCommand("npm run build");
if (buildResult !== 0) { if (buildResult !== 0) {
log("Build failed. Aborting."); log("Build failed. Aborting.");
return; return;
} }
await ask("Ready for next step...");
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step 6/${STEPS}: minifying owl...`); log(`Step 6/${STEPS}: pushing on github...`);
const minifyResult = await execCommand("npm run minify");
if (minifyResult !== 0) {
log("Minify failed. Aborting.");
return;
}
await ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 7/${STEPS}: pushing on github...`);
const pushResult = await execCommand("git push"); const pushResult = await execCommand("git push");
if (pushResult !== 0) { if (pushResult !== 0) {
log("git push failed. Aborting."); log("git push failed. Aborting.");
return; return;
} }
await ask("Ready for next step...");
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step 8/${STEPS}: publishing release notes on github...`);
const options = {
tag_name: `v${next}`,
name: `v${next}`,
body: content,
draft: true // todo: remove this someday
};
const result = await createRelease(token, options);
await ask("Ready for next step...");
// --------------------------------------------------------------------------- log(`Step 7/${STEPS}: Creating the release...`);
log(`Step 9/${STEPS}: adding assets to release...`); const relaseResult = await execCommand(`gh release create v${next} dist/*.js ${draft} -F release-notes.md`);
await ask("Please add owl.js and owl.min.js to draft release, then confirm"); if (relaseResult !== 0) {
// todo: do this with curl log("github release failed. Aborting.");
// curl \ return;
// -H "Authorization: token $GITHUB_TOKEN" \ }
// -H "Content-Type: $(file -b --mime-type $FILE)" \
// --data-binary @$FILE \
// "https://uploads.github.com/repos/hubot/singularity/releases/123/assets?name=$(basename $FILE)"
// --------------------------------------------------------------------------- log(`Step 8/${STEPS}: publishing module on npm...`);
log(`Step 10/${STEPS}: publishing module on npm...`);
await execCommand("npm run publish"); await execCommand("npm run publish");
log("Owl Release process completed! Thank you for your patience"); log("Owl Release process completed! Thank you for your patience");
await execCommand(`gh release view`);
await execCommand(`gh release view -w`);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -199,16 +177,3 @@ async function replaceInFile(file, from, to) {
}); });
}); });
} }
function createRelease(token, options) {
return new Promise((resolve, reject) => {
var gh = new GitHub({ token });
gh.getRepo("odoo", "owl").createRelease(options, (err, result, req) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}
+1 -1
View File
@@ -16,7 +16,7 @@ URL = 'http://{0}:{1}/tools'.format(HOST, PORT)
class OWLHandler(SimpleHTTPRequestHandler): class OWLHandler(SimpleHTTPRequestHandler):
def do_GET(self): def do_GET(self):
if self.path == '/tools/owl.js': if self.path == '/tools/owl.js':
self.path = '/dist/owl.js' self.path = '/dist/owl.iife.js'
return SimpleHTTPRequestHandler.do_GET(self) return SimpleHTTPRequestHandler.do_GET(self)
def end_headers(self): def end_headers(self):
+104 -17
View File
@@ -1,20 +1,107 @@
{ {
/**
** Commented-out options have their default values.
**/
"include": [
"src/**/*.ts",
"src/*.ts"
],
                                                              // "exclude": [],
// "files": [],                   // A list of relative or absolute file paths to include.
// "extends": "",                   // A string containing a path to another configuration file to inherit from.
// "references": [],                   // An array of objects `{"path": "./to/dirOrConfig"}` that specifies projects to reference.
// "compileOnSave": false,                   // Signals to the IDE to generate all files for a given tsconfig.json upon saving.
"compilerOptions": { "compilerOptions": {
"module": "commonjs",                                                             // Main options
"preserveConstEnums": true, "target": "esnext",                                         // Specify ECMAScript target version: 'es3' (default), 'es5', 'es2015', 'es2016', 'es2017','es2018' or 'esnext'.
"noImplicitThis": true, "module": "esnext",                                         // Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
"removeComments": false, // "lib": ["esnext", "dom"],                 // Specify library files to be included in the compilation.
"declaration": true, // "allowJs": false,                 // Allow javascript files to be compiled.
"target": "esnext", // "checkJs": false,                 // Report errors in .js files.
"outDir": "dist", // "outFile": "./",                 // Concatenate and emit output to single file.
"alwaysStrict": true, "outDir": "dist",                                           // Redirect output structure to the directory.
"noUnusedLocals": true, // "rootDir": "./",                 // Specify the root directory of input files. Use to control the output directory structure with `--outDir`.
"noUnusedParameters": false, // "project": "",                 // Compile a project given a valid configuration file.
"noImplicitReturns": true,                                                             // Compilation options
"noFallthroughCasesInSwitch": true, // "composite": true,                 // Enable project compilation
"strictPropertyInitialization": true, // "diagnostics": false,                 // Show diagnostic information.
"strictNullChecks": true, // "incremental": true,                 // Enable incremental compilation by reading/writing information from prior compilations to a file on disk.
"declarationDir": "dist/types" // "isolatedModules": false,                 // Transpile each file as a separate module (similar to 'ts.transpileModule').
}, // "listEmittedFiles": false,                 // Print names of generated files part of the compilation.
"include": ["src/**/*.ts","src/*.ts"] // "listFiles": true,                 // Print names of files part of the compilation.
// "noErrorTruncation": false,                 // Do not truncate error messages.
// "preserveWatchOutput": false,                 // Keep outdated console output in watch mode instead of clearing the screen.
// "traceResolution": false,                 // Enable tracing of the name resolution process.
// "tsBuildInfoFile": ".tsbuildinfo",                 // Specify file to store incremental compilation information.
                                                            // Strict typechecking options
// "strict": false,                                         // Enable all strict type-checking options.
// "noImplicitAny": true,                 // Raise error on expressions and declarations with an implied 'any' type.
// "noImplicitThis": true,                 // Raise error on 'this' expressions with an implied 'any' type.
// "strictBindCallApply": true,                 // Enable stricter checking of of the `bind`, `call`, and `apply` methods on functions.
// "strictFunctionTypes": true,                 // Disable bivariant parameter checking for function types.
// "strictNullChecks": true,                 // In strict null checking mode, the null and undefined values are not in the domain of every type and are only assignable to themselves and any.
// "strictPropertyInitialization": true,                 // Ensure non-undefined class properties are initialized in the constructor. This option requires `--strictNullChecks` be enabled in order to take effect.
// "alwaysStrict": true,                 // Parse in strict mode and emit "use strict" for each source file.
                                                            // Additional checks
// "allowUnreachableCode": false,                 // Do not report errors on unreachable code.
// "allowUnusedLabels": false,                 // Do not report errors on unused labels.
"forceConsistentCasingInFileNames": true,                   // Disallow inconsistently-cased references to the same file.
// "noStrictGenericChecks": false,                 // Disable strict checking of generic signatures in function types.
"noUnusedLocals": true,                                     // Report errors on unused locals.
"noUnusedParameters": false,                                // Report errors on unused parameters.
"noImplicitReturns": true,                                  // Report error when not all code paths in function return a value.
"noFallthroughCasesInSwitch": true,                         // Report errors for fallthrough cases in switch statement.
// "skipLibCheck": false,                 // Skip type checking of all declaration files (*.d.ts).
// "suppressExcessPropertyErrors": false,                 // Suppress excess property checks for object literals.
// "suppressImplicitAnyIndexErrors": false,                 // Suppress noImplicitAny errors for indexing objects lacking index signatures.
                                                            // Module resolution options
"moduleResolution": "node",                                 // Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6).
// "baseUrl": "./",                 // Base directory to resolve non-absolute module names.
// "paths": {},                 // A series of entries which re-map imports to lookup locations relative to the 'baseUrl'.
// "rootDirs": [],                 // List of root folders whose combined content represents the structure of the project at runtime.
// "typeRoots": [],                 // List of folders to include type definitions from.
"types": [
"jest",
"node"
],                                                // Type declaration files to be included in compilation.
// "allowSyntheticDefaultImports": false                    // Allow default imports from modules with no default export. This does not affect code emit, just typechecking.
"esModuleInterop": true,                  // Emit '__importStar' and '__importDefault' helpers for runtime babel ecosystem compatibility and enable '--allowSyntheticDefaultImports' for typesystem compatibility.
// "maxNodeModuleJsDepth": 0,                 // The maximum dependency depth to search under node_modules and load JavaScript files. Only applicable with --allowJs.
// "preserveSymlinks": false,                 // Do not resolve the real path of symlinks.
"resolveJsonModule": true,                                  // Include modules imported with '.json' extension.
                                                            // Emit options
"declaration": true,                                        // Generates corresponding '.d.ts' file.
"declarationDir": "dist/types",                             // Output directory for generated declaration files.
// "declarationMap": false,                 // Generates a sourcemap for each corresponding '.d.ts' file.
// "emitBOM": false,                 // Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files.
// "emitDeclarationOnly": false,                 // Only emit .d.ts declaration files.
// "importHelpers": false,                 // Import emit helpers from 'tslib'.
// "newLine": "LF",                 // Use the specified end of line sequence to be used when emitting files: "crlf" (windows) or "lf" (unix).
// "noEmit": true,                 // Do not emit outputs.
// "noEmitHelpers": false,                 // Do not generate custom helper functions like __extends in compiled output.
// "noEmitOnError": false,                 // Do not emit outputs if any errors were reported.
// "noImplicitUseStrict": false,                 // Do not emit "use strict" directives in module output.
// "noResolve": false,                 // Do not add triple-slash references or module import targets to the list of compiled files.
"preserveConstEnums": true,                                 // Do not erase const enum declarations in generated code.
// "removeComments": false,                 // Remove all comments except copy-right header comments beginning with
// "experimentalDecorators": true,                 // Enables experimental support for ES7 decorators.
// "emitDecoratorMetadata": true,                 // Enables experimental support for emitting type metadata for decorators.
                                                            // Source map options
// "sourceMap": false,                 // Generates corresponding '.map' file.
// "sourceRoot": "",                 // Specify the location where debugger should locate TypeScript files instead of source locations.
// "mapRoot": "",                 // Specify the location where debugger should locate map files instead of generated locations.
// "inlineSourceMap": true,                 // Emit a single file with source maps instead of having a separate file.
// "inlineSources": true,                 // Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set.
                                                            // JSX options
// "jsx": "preserve",                 // Specify JSX code generation: 'preserve', 'react-native', or 'react'.
// "jsxFactory": "React.createElement",                 // Specify the JSX factory function to use when targeting react JSX emit, e.g. 'React.createElement' or 'h'.
                                                            // Other options
// "allowUmdGlobalAccess": true,                 // Allow accessing UMD globals from modules.
// "charset": "utf8",                 // The character set of the input files.
// "downlevelIteration": false,                 // Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'.
// "disableSizeLimit": false,                 // Disable size limitation on JavaScript project.
// "keyofStringsOnly": false,                 // Resolve 'keyof' to string valued property names only (no numbers or symbols).
// "noLib": false,                 // Do not include the default library file (lib.d.ts).
// "pretty": true,                 // Stylize errors and messages using color and context.
}
} }