Compare commits

..

59 Commits

Author SHA1 Message Date
Géry Debongnie 3d0f046d9a [REL] v1.0.0
Finally!!
2020-01-09 14:37:36 +01:00
Géry Debongnie 4c75bae0e8 [IMP] owl: improve npm packaging
closes #611
2020-01-09 14:35:13 +01:00
Géry Debongnie 54f5819ef9 [ADD] hooks: add useExternalListener hook
It is very useful.

Also, this commit prettifies the code.

closes #608
2020-01-09 14:06:30 +01:00
Joseph Caburnay 0c9c5b877b [FIX] playground: not all completed is deleted
Before this fix, not all completed tasks are deleted when clicking clear
completed button because we are looping to a mutated array. Looping thru
a copy of the mutated array fixes this problem.
2020-01-09 09:40:13 +01:00
Joseph Caburnay b7b0e38fca [FIX] docs: some spelling corrections 2020-01-09 09:40:13 +01:00
mcm-odoo 74bd119bf5 [IMP] styles: handle & selector
This commit adds the support of the & selector.
This selector is useful to join a subrule with a parent selector.

example:

button {
  &:hover {
    background-color: red;
  }
}

will give

button:hover {
  background-color: red;
}
2020-01-09 09:37:27 +01:00
Géry Debongnie 4a0d04e4ae [FIX] debug tools: do not crash on non json-stringifiable props
closes #595
2020-01-09 09:24:50 +01:00
Géry Debongnie 49931104a1 [CLEANUP] update snapshots 2020-01-08 16:05:22 +01:00
Lucas Perais (lpe) 21d1306ec2 [FIX] qweb: t-foreach needs to hold on scope
Have a t-on within a t-foreach
the t-on has an expression

Before this commit, when triggering the t-on, the expression was falsely evaluated
In details, the expression took scope from outside the foreach loop
as we protect it to have similar results than an actual for loop

But, at triggering time, the scope protection had already been terminated
meaning the info of the iteration of the loop the handler has been built in
had already disappeared

This commit fixes that

closes #594
2020-01-08 15:45:44 +01:00
Lucas Perais (lpe) 21737d33fa [FIX] qweb: variables set outside foreach must be altered by inloop t-tset
In a template, have t-set t-value outside a t-foreach
in the t-foreach, alter that variable by resetting it (as for a incrementation variable)

Before this commit, when printing the variable when the loop had finished
its value was the one set in the first place

After this commit, the value becomes the one altered by the loop iterations

closes #598
2020-01-08 15:45:44 +01:00
Géry Debongnie 23012f3e7c [FIX] component: multiple t-calls
Before this commit, owl could crash in some specific situations:
multiple t-calls with sub components, outside a loop.  The reason is
that the t-call did not generate a key, so from the point of view of the
children component, it had the same key, even though it was at a
different place in a template.

Also, with this fix, we can fix the playground responsive example.

closes #602
2020-01-08 15:24:43 +01:00
Géry Debongnie 55889febd5 [FIX] qweb: add support for "in " operator in expressions
closes #603
2020-01-08 15:20:01 +01:00
mcm-odoo 0af46bc123 [FIX] styles: fix selector generation
This commit fixes the css selector generation for nested rules

Before this fix, a selector like:

.parent {
	.child-a, .child-b {
		color: red;
	}
}

was generated as:

.parent .child-a, .child-b {
	color: red;
}

Now it generates:

.parent .child-a, .parent .child-b {
	color: red;
}
2019-12-27 12:31:17 +01:00
Géry Debongnie b63cd4c9d9 [REL] v1.0.0-beta5 2019-12-20 11:26:53 +01:00
Géry Debongnie 953778dc50 [IMP] component/tags: add inline css tag
This add an important feature: defining completely standalone owl
components, with the template/style and javascript code together.

closes #284
2019-12-20 11:20:10 +01:00
Géry Debongnie 4f61d9f1e0 [FIX] component: no crash if empty props
The expression parser properly format an empty string into an empty
string. But then, this empty string was injected in the props object of
a component, and the compiled code looked like this:

let props5 = { val: };

So, in this case, we simply put undefined, since there are no props
value.

closes #587
2019-12-19 14:32:10 +01:00
Sébastien Theys bc2c7edff4 [FIX] store: prevent crash when selector returns null
Because `typeof null === 'object'`.
2019-12-19 14:24:36 +01:00
Géry Debongnie c8d9c0b50e [DOC] add npm install instruction 2019-12-19 14:12:00 +01:00
Géry Debongnie 15b25fd838 [IMP] owl: prepare owl to be released on npm
closes #196
2019-12-19 14:09:41 +01:00
Géry Debongnie 1db0f5ac9b [REL] v1.0.0-beta4 2019-12-17 15:26:49 +01:00
Géry Debongnie 6465665550 [FIX] qweb/component: handle nested t-foreach and components
The way keys were handled in QWeb was mostly ok, but a little naive. It
is fine when we deal with a list of dom nodes, since the reconciliation
algorithm need only to be able to differentiate/reconcile nodes in that
list, but it is an issue with components, which needs to globally be
able to find themselves in their parent's children map.

Because of the way this was handled, there were situations were Owl
internal virtual dom would crash, since components wrongly assumed that
they were already rendered in a different place.

To fix this, we generalize the way keys are generated, by concatenating
all sub keys coming from each iteration loops.

closes #584
2019-12-17 15:23:24 +01:00
Géry Debongnie 9216c5c24b [FIX] qweb: t-call properly transfer key to sub components
Since t-call is now a function call, we need to properly handle the
internal key used to find previous components.  If a t-call is inside a
t-foreach, then we need to transfer the key to the sub template.
Otherwise, each component in the subtemplate will be associated to the
same key, which means that it will lead to big issues: components are
destroyed and reused...

closes #581
2019-12-16 20:30:34 +01:00
Géry Debongnie d9bf4284c6 [REL] v1.0.0-beta3 2019-12-16 09:56:35 +01:00
Géry Debongnie ef6fd457f8 [FIX] qweb: add support for spread operator
closes #575
2019-12-13 22:35:26 +01:00
Sébastien Theys 455e45c148 [FIX] store: make Array.map work correctly on the result of selector
closes #577
2019-12-13 20:32:02 +01:00
Lucas Perais (lpe) 5e7f1d5e6d [FIX] qweb: t-call must pass extra parent
Before this commit, when template was t-call'ed and defined
t-component directive within it, there was a crash because extra.parent was never defined

After this commit, this use case works
2019-12-13 20:28:49 +01:00
Géry Debongnie 0b05ad619f [FIX] qweb: t-set with a body should not be joined with comma
closes #574
2019-12-13 20:15:37 +01:00
Géry Debongnie 29a80d0b28 [FIX] qweb: properly display falsy values
While it is not tested, nor documented, the reference QWeb
implementation display the `false` value as "false". So, we have to
adapt the Owl implementation to match that behaviour.

At the same time, this commit uses 'let' instead of 'var' in various places,
to make the compiled code more consistant.
2019-12-13 14:48:19 +01:00
Géry Debongnie 0931a4dc5b [FIX] qweb: bind handlers to component
Before this commit, the handlers were bound to the current context,
which is often the component but not necessarily.  In some cases, a sub
scope can be created (with t-foreach, or slots, or t-call), and the
context is actually an object with the actual component instance it its
prototype chain, but not the component.
2019-12-12 14:32:43 +01:00
Géry Debongnie 4e22dbcad6 [FIX] qweb: accept assignations in qweb expressions
closes #560
closes #569
2019-12-12 14:32:43 +01:00
Géry Debongnie b283e65ad4 [REF] qweb: improve t-on generated code
and deduplicate logic between directive t-on applied on a node and on a
component
2019-12-12 14:32:43 +01:00
Géry Debongnie f517d44e32 [IMP] qweb: slightly simplify generated templates
- In some cases, we can simply inline a template key instead of assigning
  it to a variable and then using that variable
- Add component name in generated code for components
2019-12-12 09:39:26 +01:00
Lucas Perais (lpe) 08022b49df [FIX] qweb: conditional t-call with body
When a t-call has a condition and a body
the t-if's and its flavors should be ignored when compiling the body of a t-call
2019-12-11 14:09:45 +01:00
Géry Debongnie b96ea79f2b [FIX] qweb: renderToString now properly escape
closes #565
2019-12-11 12:25:05 +01:00
Géry Debongnie 97d8b3ed8c [REF] tests: remove Widget common class
Bad name, and bad practice to use it. It makes tests less standalone.
2019-12-11 12:25:05 +01:00
Géry Debongnie 9921ae07b1 [REF] tests: move slots tests out of main component file 2019-12-11 12:25:05 +01:00
Géry Debongnie 41cb5f1abd [FIX] *: remove useless values to t-else and t-debug
The actual value does not matter, only the fact that it is present.

closes #564
2019-12-11 12:25:05 +01:00
Géry Debongnie 0762eeb010 [REL] v1.0.0-beta2 2019-12-11 10:19:04 +01:00
Géry Debongnie bc455be720 [IMP] components: slots can now define default content
closes #554
2019-12-11 10:11:47 +01:00
Lucas Perais (lpe) 9145799ae9 [FIX] qweb: evaluate body of t-set immediately 2019-12-11 10:07:41 +01:00
Géry Debongnie ce9c2f8613 [FIX] qweb/component: refactoring of scoping/variables/slots
This commit is a significant refactoring of the internal of QWeb. It
simplifies the way variables/scoping and slots interact together.  The
main idea is that we use a simple scope object instead of a
context/var/scope object.

This commit also implement the actual correct QWeb semantic for the
t-call directive with a sub body.  Before, we simply extracted the
variables from the body and injected them at the top of the sub
template.  We now simply compile the body before the sub template.

This is a joint work with Lucas (lpe).

closes #541
closes #544
closes #545

closes #557
closes #556
2019-12-11 10:07:41 +01:00
Géry Debongnie b890e7ceae [IMP] qweb: add template name in generated compiled code 2019-12-10 16:14:09 +01:00
Géry Debongnie 124211c18b [DOC] clarify store documentation on connected components
closes #538
2019-12-06 12:25:55 +01:00
Géry Debongnie be8c3bbf6f [IMP] tooling: add warning if component not mounted, but rendered
closes #551
2019-12-06 12:07:54 +01:00
Géry Debongnie e162a6fb6c [IMP] qweb: add support for arrow functions in expression parser
closes #542
2019-12-06 11:49:55 +01:00
Géry Debongnie 7ec05ff8cf [IMP] component: add position option to mount method
A component can now be mounted in different positions: either 'before' (at
the start), 'after' (at the end), or 'attach' (take possession of a target
element)

closes #539
2019-12-06 10:53:01 +01:00
Géry Debongnie a0bfbf6dd0 [FIX] qweb/component: do not call handlers for unmounted components
Unmounted components should be considered inactive, at least from the
perspective of Owl itself.

closes #543
2019-12-06 09:58:12 +01:00
Géry Debongnie b67cb71048 [FIX] slots: propagate vars to correct function
closes #529
2019-12-06 09:53:48 +01:00
Géry Debongnie f5b3ef5cf3 [DOC] add some information on variables and t-call
closes #540
2019-12-05 21:49:51 +01:00
Lucas Perais (lpe) ecfccf2448 [FIX] portal: manually unmounting works
Before this commit, when manually unmounting the portal
or one of its parents, the teleported elements were not cleaned
and stayed teleported forever

After this commit, the teleported content is brought back and
unmounted much like any component

closes #531
2019-12-04 09:48:08 +01:00
Géry Debongnie f12d3373c9 [FIX] qweb: properly handle empty class attribute
The classList.add method actually crashes when given an empty string (or
a string with just white spaces).

closes #530
2019-12-03 16:19:09 +01:00
Géry Debongnie 286090efca [FIX] tooling: debug tool logged too many scheduler stops
This was caused by additional calls to flush (because of connected
components)

closes #533
2019-12-03 15:16:29 +01:00
Géry Debongnie 2b4ff7b2fd [FIX] doc: the debugOwl helper was missing a parameter
closes #532
2019-12-03 14:13:42 +01:00
Géry Debongnie c43178a45b [CLEANUP] observer: remove useless variable 2019-12-03 11:17:37 +01:00
Géry Debongnie 033a431e18 [ADD] tools: add benchmark for 1.0.0-beta1, remove old ones 2019-12-03 09:12:52 +01:00
Géry Debongnie 42aa9d3ae1 [REL] v1.0.0-beta1 2019-12-03 08:52:04 +01:00
Géry Debongnie 85f26a0286 [IMP] tools: update debug script, add tests
closes #525
closes #526
2019-12-03 08:28:46 +01:00
Géry Debongnie ed4ab51c17 [FIX] component: tricky concurrency issue
This is a really interesting problem: there was a situation where a
component would not have an event handler properly bound. The reason was
that we were in a situation where the scheduler task queue was flushed
at an unfortunate timing:

- parent component template is rendered
- subcomponent is prepared:
   - sub component is willStarted
   - it is rendered (so, fiber counter is set to 0)
- scheduler task queue is flushed => we patch the DOM
- the code registered in the __prepare.then(handler) is executed, which
  add the createHook to the vnode (but after the dom is patched)

Usually, the last two steps happen in a different order, but in an
environment with connected components, we sometimes flush the task queue
at various moments, so some code could be executed between the end of
__prepare and the registered handler.

The fix is interesting: we give a callback to the __prepare function
instead of registering a .then handler to the deferred.  This callback
is then guaranteed to be called at exactly the proper timing.

Thanks seb for the hard work of finding the root cause of this issue

closes #520
2019-12-03 08:23:10 +01:00
Lucas Perais (lpe) 556a4644f0 [ADD] misc: component Portal
The component Portal is used to teleport the content
in its slot as a child of an element
somewhere else in the DOM, usually 'body'

It is designed to be transparent and is just here to do the teleportation task

OwlEvents (with method `Component.trigger`) are redirected from
any Component instanciated within the Portal (and therefore teleported
somewhere else) to the place it would have been without teleportation
i.e. they are re-triggered onto the Portal root node

Co-authored-by: Aaron Bohy <aab@odoo.com>

closes #184
closes #466
2019-12-02 21:04:07 +01:00
78 changed files with 12683 additions and 18725 deletions
+8 -2
View File
@@ -102,10 +102,16 @@ Submit a PR!
## Installing/Building
Owl can be installed with the following command:
```
npm install @odoo/owl
```
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.0.0-alpha5.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha5/owl.js)
- [owl-1.0.0-alpha5.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha5/owl.min.js)
- [owl-1.0.0.js](https://github.com/odoo/owl/releases/download/v1.0.0/owl.js)
- [owl-1.0.0.min.js](https://github.com/odoo/owl/releases/download/v1.0.0/owl.min.js)
Some npm scripts are available:
+15 -15
View File
@@ -50,21 +50,21 @@ and `EventBus` is exported as `owl.core.EventBus`):
```
Component misc
Context AsyncRoot
QWeb router
Store Link
useState RouteComponent
config Router
mode tags
core xml
EventBus utils
Observer debounce
hooks escape
onWillStart loadJS
onMounted loadFile
onWillUpdateProps shallowEqual
onWillPatch whenReady
onPatched
onWillUnmount
QWeb Portal
Store router
useState Link
config RouteComponent
mode Router
core tags
EventBus css
Observer xml
hooks utils
onWillStart debounce
onMounted escape
onWillUpdateProps loadJS
onWillPatch loadFile
onPatched shallowEqual
onWillUnmount whenReady
useContext
useState
useRef
+46 -8
View File
@@ -225,6 +225,10 @@ to be called in the constructor.
}
```
- **`style`** (string, optional): it should be the return value of the [`css` tag](tags.md#css-tag),
which is used to inject stylesheet whenever the component is visible on the
screen.
There is another static property defined on the `Component` class: `current`.
This property is set to the currently being defined component (in the constructor).
This is the way [hooks](hooks.md) are able to get a reference to the target
@@ -234,12 +238,23 @@ component.
We explain here all the public methods of the `Component` class.
- **`mount(target)`** (async): this is the main way a
- **`mount(target, options)`** (async): this is the main way a
component is added to the DOM: the root component is mounted to a target
HTMLElement (or document fragment). Obviously, this is asynchronous, since each children need to be
created as well. Most applications will need to call `mount` exactly once, on
the root component.
The `options` argument is an optional object with a `position` key. The
`position` key can have three possible values: `first-child`, `last-child`, `self`.
- `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`.
Note that if a component is mounted, unmounted and remounted, it will be
automatically re-rendered to ensure that changes in its state (or something
in the environment, or in the store, or ...) will be taken into account.
@@ -258,25 +273,25 @@ We explain here all the public methods of the `Component` class.
// app is now visible
```
- **`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
useful to the underlying component system.
- **`render()`** (async): calling this method directly will cause a rerender. Note
* **`render()`** (async): calling this method directly will cause a rerender. Note
that this should be very rare to have to do it manually, the Owl framework is
most of the time responsible for doing that at an appropriate moment.
Note that the render method is asynchronous, so one cannot observe the updated
DOM in the same stack frame.
- **`shouldUpdate(nextProps)`**: this method is called each time a component's props
* **`shouldUpdate(nextProps)`**: this method is called each time a component's props
are updated. It returns a boolean, which indicates if the component should
ignore a props update. If it returns false, then `willUpdateProps` will not
be called, and no rendering will occur. Its default implementation is to
always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
can be useful if we are handling large number of components.
- **`destroy()`**. As its name suggests, this method will remove the component,
* **`destroy()`**. As its name suggests, this method will remove the component,
and perform all necessary cleanup, such as unmounting the component, its children,
removing the parent/children relationship. This method should almost never be
called directly (except maybe on the root component), but should be done by the
@@ -410,7 +425,7 @@ likely via a change in its state/props or environment).
This method is not called on the initial render. It is useful to interact
with the DOM (for example, through an external library) whenever the
component was patched. Note that this hook will not be called if the compoent is
component was patched. Note that this hook will not be called if the component is
not in the DOM.
Updating the component state in this hook is possible, but not encouraged.
@@ -617,10 +632,20 @@ as it will be translated to:
```js
button.addEventListener("click", () => {
component.state.counter++;
context.state.counter++;
});
```
Warning: inline expressions are evaluated in the context of the template. This
means that they can access the component methods and properties. But if they set
a key, the inline statement will actually not modify the component, but a key in
a sub scope.
```xml
<button t-on-click="value = 1">Set value to 1 (does not work!!!)</button>
<button t-on-click="state.value = 1">Set state.value to 1 (work as expected)</button>
```
In order to remove the DOM event details from the event handlers (like calls to
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
specified as additional suffixes of the `t-on` directive.
@@ -854,6 +879,19 @@ be considered the `default` slot. For example:
</div>
```
Slots can define a default content, in case the parent did not define them:
```xml
<div t-name="Parent">
<Child/>
</div>
<span t-name="Child">
<t t-slot="default">default content</t>
</span>
<!-- will be rendered as: <div><span>default content</span></div> -->
```
### Dynamic sub components
It is not common, but sometimes we need a dynamic component name. In this case,
@@ -922,7 +960,7 @@ For example, here is how we could implement an `ErrorBoundary` component:
<t t-if="state.error">
Error handled
</t>
<t t-else="1">
<t t-else="">
<t t-slot="default" />
</t>
</div>
+2 -2
View File
@@ -23,7 +23,7 @@ a rendering that is no longer relevant, restart it, reuse it in some cases.
But even though using concurrency is quite simple (and is the default behaviour),
asynchrony is difficult, because it introduces an additional dimension that
vastly increase the complexity of an application. This section will explain
how Owl manages this complexity, how concuurent rendering works in a general way.
how Owl manages this complexity, how concurrent rendering works in a general way.
## Rendering Components
@@ -120,7 +120,7 @@ Here is what Owl will do:
1. hook `willUpdateProps` is called on `D` (async)
2. template `D` is rerendered
- component `F` is created:
1. hook `willStart` is called on `E` (async)
1. hook `willStart` is called on `F` (async)
2. template `F` is rendered
3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`,
+1 -1
View File
@@ -57,7 +57,7 @@ class SomeComponent extends Component {
<t t-if=device.isMobile>
some simplified user interface
</t>
<t t-else="1">
<t t-else="">
a more advanced user interface
</t>
</div>`;
+2 -2
View File
@@ -40,8 +40,8 @@ all templates.
Whenever a root component `App` is mounted, Owl will setup a valid environment by
following the next steps:
- take the `env` object defined on `App.env` (if no `env` was explicitely setup,
this will be return the empty `env` object defined on `Component`)
- take the `env` object defined on `App.env` (if no `env` was explicitly setup,
this will return the empty `env` object defined on `Component`)
- if `env.qweb` is not set, then Owl will create a `QWeb` instance.
The correct way to customize an environment is to simply set it up on the root
+12
View File
@@ -17,6 +17,7 @@
- [`useContext`](#usecontext)
- [`useRef`](#useref)
- [`useSubEnv`](#usesubenv)
- [`useExternalListener`](#useexternallistener)
- [`useStore`](#usestore)
- [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters)
@@ -354,6 +355,17 @@ will be added to the parent environment. Note that it will extend, not replace
the parent environment. And of course, the parent environment will not be
affected.
### `useExternalListener`
The `useExternalListener` hook helps solve a very common problem: adding and removing
a listener on some target whenever a component is mounted/unmounted. For example,
a dropdown menu (or its parent) may need to listen to a `click` event on `window`
to be closed:
```js
useExternalListener(window, "click", this.closeMenu);
```
### `useStore`
The `useStore` hook is the entry point for a component to connect to the store.
+111
View File
@@ -1,5 +1,116 @@
# 🦉 Miscellaneous 🦉
## Content
- [Portal](#portal)
- [AsyncRoot](#asyncroot)
## `Portal`
### Overview
The component `Portal` is meant to be used as a transparent way to 'teleport' a piece
of DOM to the node represented by its sole `target` props.
This component aims at helping the implementation of the needed infrastructure
for modals (as in `bootstrap-modal`).
### Usage
The content it will teleport is defined within the `<Portal>` node and
internally uses the `default` [Slot](component.md#slots).
This slot must contain only **one** node, which in turn can have as many children as necessary.
The element under which the content will be teleported is represented as a selector
by the `target` props which only accepts a string as value.
The `target` props only supports static selector, and is not meant to be passed to `Portal`
as a variable. Namely, `<Portal target="'body'" />` is the intended use.
By contrast, `<Portal target="state.target" />` is not supported.
The component `Portal` has no particular state, rather it is meant to be a slave to its parent,
and ultimately just a way for the parent to teleport a piece of its own DOM elsewhere.
The `Portal`'s root node is always `<portal/>` and is placed where the teleported content
_would have_ been. It is this element that the [teleported events](#expected-behaviors) are re-directed on.
### Example
The canonic use-case is to implement a Dialog, where a Component may choose to break the natural
workflow to help the user put in some data, which it could use later on.
JavaScript:
```js
const { Component } = owl;
const { Portal } = owl.misc;
class TeleportedComponent extends Component {}
class App extends Component {
static components = { Portal, TeleportedComponent };
}
const app = new App();
app.mount(document.body);
```
XML:
```xml
<templates>
<div t-name="TeleportedComponent">
<span>I will move soon enough</span>
</div>
<div t-name="App">
<span>I am like the rest of us</span>
<Portal target="'body'">
<TeleportedComponent />
</Portal>
</div>
</templates>
```
In this example, the `Portal` component will teleport the `TeleportedComponent`'s `div` as a child of the `body`.
`TeleportedComponent` is acting as a Dialog here.
The resulting DOM will look like:
```xml
<body>
<div>
<span>I am like the rest of us</span>
<portal></portal>
</div>
<div>
<span>I will move soon enough</span>
</div>
</body>
```
### Expected Behaviors
The teleported piece is updated as any other `Component`'s DOM and in the same sequence.
Namely the teleported piece will be updated in function of its parents components, and patched as
a normal child.
The [_business_ events](component.md#event-handling) triggered by a child component will be stopped
to not bubble outside of the `target`. They will, on the other hand, be re-directed onto the
`Portal`'s root node and bubble up the DOM as if it were triggered by a regular child component.
Beware that those re-directed events are copies of the original event.
They have:
- The same payload.
- The same `originalComponent` than their original counterpart,
that is the actual Component that triggered it.
- A **different** `target` property than their original counterpart.
The `target` of a re-directed event is necessarily the `Portal`'s root node.
Pure DOM events do not follow this pattern and are free to bubble their natural, unaltered way
up to the `body`.
## `AsyncRoot`
When this component is used, a new rendering sub tree is created, such that the
+11 -2
View File
@@ -28,7 +28,7 @@ generate a virtual dom representation of the HTML.
```xml
<div>
<span t-if="somecondition">Some string</span>
<ul t-else="1">
<ul t-else="">
<li t-foreach="messages" t-as="message">
<t t-esc="message"/>
</li>
@@ -101,7 +101,7 @@ precisely, the result of a template rendering should have a single root node:
<!–– ok: result has one single root node ––>
<t>
<div t-if="someCondition">foo</div>
<span t-else="1">bar</span>
<span t-else="">bar</span>
</t>
```
@@ -500,6 +500,15 @@ will result in :
</div>
```
This can be used to define variables scoped to a sub template:
```xml
<t t-call="other-template">
<t t-set="var" t-value="1"/>
</t>
<!-- "var" does not exist here -->
```
### Translations
By default, QWeb specify that templates should be translated. If this behaviour
+2 -2
View File
@@ -25,8 +25,8 @@ component should own which part of the state.
Owl's solution to this issue is a centralized store. It is a class that owns
some (or all) state, and lets the developer update it in a structured way, with
`actions`. Owl components can then connect to the store, and will be updated if
necessary.
`actions`. Owl components can then connect to the store to read their relevant
state, and they will be rerendered if the state is updated.
Note: Owl store is inspired by React Redux and VueX.
+134 -4
View File
@@ -4,16 +4,19 @@
- [Overview](#overview)
- [`xml` tag](#xml-tag)
- [`css` tag](#css-tag)
## Overview
Tags are very small helpers to make it easy to write inline templates. There is
only one currently available tag: `xml`, but we plan to add other tags later,
such as a `css` tag, which will be used to write [single file components](../tooling.md#single-file-component).
Tags are very small helpers intended to make it easy to write inline templates
or styles. There are currently two tags: `css` and `xml`. With these functions,
it is possible to write [single file components](../tooling.md#single-file-component).
## XML tag
Without tags, creating a standalone component would look like this:
The `xml` tag is certainly the most useful tag. It is used to define an inline
QWeb template for a component. Without tags, creating a standalone component
would look like this:
```js
import { Component } from 'owl'
@@ -52,3 +55,130 @@ class MyComponent extends Component {
...
}
```
## CSS tag
The CSS tag is useful to define a css stylesheet in the javascript file:
```js
class MyComponent extends Component {
static template = xml`
<div class="my-component">some template</div>
`;
static css`
.my-component {
color: red;
}
`;
}
```
The `css` tag registers internally the css information. Then, whenever the first
instance of the component is created, will add a `<style>` tag to the document
`<head>`.
Note that to make it more useful, like other css preprocessors, the `css` tag
accepts a small extension of the css specification: css scopes can be nested,
and the rules will then be expanded by the `css` helper:
```scss
.my-component {
display: block;
.sub-component h {
color: red;
}
}
```
will be formatted as:
```css
.my-component {
display: block;
}
.my-component .sub-component h {
color: red;
}
```
This extension brings another useful feature: the `&` selector which refers to
the parent selector. For example, we want our component to be red when hovered.
We would like to write something like:
```scss
.my-component {
display: block;
:hover {
color: red;
}
}
```
but it will be formatted as:
```css
.my-component {
display: block;
}
.my-component :hover {
color: red;
}
```
The `&` selector can be used to solve this problem:
```scss
.my-component {
display: block;
&:hover {
color: red;
}
}
```
will be formatted as:
```css
.my-component {
display: block;
}
.my-component:hover {
color: red;
}
```
Now, there is no additional processing done by the `css` tag. However, since it
is done in javascript at runtime, we actually have more power. For example:
1. sharing values between javascript and css:
```js
import { theme } from "./theme";
class MyComponent extends Component {
static template = xml`<div class="my-component">...</div>`;
static style = css`
.my-component {
color: ${theme.MAIN_COLOR};
background-color: ${theme.SECONDARY_color};
}
`;
}
```
2. scoping rules to the current component:
```js
import { generateUUID } from "./utils";
const uuid = generateUUID();
class MyComponent extends Component {
static template = xml`<div data-o-${uuid}="">...</div>`;
static style = css`
[data-o-${uuid}] {
color: red;
}
`;
}
```
+23 -13
View File
@@ -43,32 +43,43 @@ useful to compare various performance metrics on some tasks.
It is very useful to group code by feature instead of by type of file. It makes
it easier to scale application to larger size.
To do so, Owl currently has a small helper that makes it easy to define a
template inside a javascript (or typescript) file: the [`xml`](reference/tags.md#xml-tag)
helper. With this, a template is automatically registered to [QWeb](reference/qweb_engine.md).
To do so, Owl has two small helpers that make it easy to define a
template or a stylesheet inside a javascript (or typescript) file: the
[`xml`](reference/tags.md#xml-tag) and [`css`](reference/tags.md#css-tag)
helper.
This means that the template and the javascript code can be defined in the same
file. It is not currently possible to add css to the same file, but Owl may
get a `css` tag helper later.
This means that the template, the style and the javascript code can be defined in
the same file. For example:
```js
const { Component } = owl;
const { xml } = owl.tags;
const { xml, css } = owl.tags;
// -----------------------------------------------------------------------------
// TEMPLATE
// -----------------------------------------------------------------------------
const TEMPLATE = xml/* xml */ `
<div class="main two-columns">
<div class="main">
<Sidebar/>
<Content />
</div>`;
// -----------------------------------------------------------------------------
// STYLE
// -----------------------------------------------------------------------------
const STYLE = css/* css */ `
.main {
display: grid;
grid-template-columns: 200px auto;
}
`;
// -----------------------------------------------------------------------------
// CODE
// -----------------------------------------------------------------------------
class MyComponent extends Component {
class Main extends Component {
static template = TEMPLATE;
static style = STYLE;
static components = { Sidebar, Content };
// rest of component...
@@ -91,16 +102,15 @@ logging useful information is extremely valuable. There is a [javascript file](.
Once it is executed, it will log a lot of information on each component main hooks. The following code is a minified version to make it easier to copy/paste:
```
let debugSetup = {
function debugOwl(t,e){let n,o="[OWL_DEBUG]";function r(t){let e;try{e=JSON.stringify(t||{})}catch(t){e="<JSON error>"}return e.length>200&&(e=e.slice(0,200)+"..."),e}if(Object.defineProperty(t.Component,"current",{get:()=>n,set(s){n=s;const i=s.constructor.name;if(e.componentBlackList&&e.componentBlackList.test(i))return;if(e.componentWhiteList&&!e.componentWhiteList.test(i))return;let l;Object.defineProperty(n,"__owl__",{get:()=>l,set(n){!function(n,s,i){let l=`${s}<id=${i}>`,c=t=>console.log(`${o} ${l} ${t}`),u=t=>(!e.methodBlackList||!e.methodBlackList.includes(t))&&!(e.methodWhiteList&&!e.methodWhiteList.includes(t));u("constructor")&&c(`constructor, props=${r(n.props)}`);u("willStart")&&t.hooks.onWillStart(()=>{c("willStart")});u("mounted")&&t.hooks.onMounted(()=>{c("mounted")});u("willUpdateProps")&&t.hooks.onWillUpdateProps(t=>{c(`willUpdateProps, nextprops=${r(t)}`)});u("willPatch")&&t.hooks.onWillPatch(()=>{c("willPatch")});u("patched")&&t.hooks.onPatched(()=>{c("patched")});u("willUnmount")&&t.hooks.onWillUnmount(()=>{c("willUnmount")});const d=n.__render.bind(n);n.__render=function(...t){c("rendering template"),d(...t)};const h=n.render.bind(n);n.render=function(...t){const e=n.__owl__;let o="render";return e.isMounted||e.currentFiber||(o+=" (warning: component is not mounted, this render has no effect)"),c(o),h(...t)};const p=n.mount.bind(n);n.mount=function(...t){return c("mount"),p(...t)}}(s,i,(l=n).id)}})}}),e.logScheduler){let e=t.Component.scheduler.start,n=t.Component.scheduler.stop;t.Component.scheduler.start=function(){this.isRunning||console.log(`${o} scheduler: start running tasks queue`),e.call(this)},t.Component.scheduler.stop=function(){this.isRunning&&console.log(`${o} scheduler: stop running tasks queue`),n.call(this)}}if(e.logStore){let e=t.Store.prototype.dispatch;t.Store.prototype.dispatch=function(t,...n){return console.log(`${o} store: action '${t}' dispatched. Payload: '${r(n)}'`),e.call(this,t,...n)}}}
debugOwl(owl, {
// componentBlackList: /App/, // regexp
// componentWhiteList: /SomeComponent/, // regexp
// methodBlackList: ["mounted"], // list of method names
// methodWhiteList: ["willStart"], // list of method names
logScheduler: false, // display/mute scheduler logs
logStore: true, // display/mute store logs
};
{let o,t="[OWL_DEBUG]";function toStr(o){let t=JSON.stringify(o||{});return t.length>200&&(t=t.slice(0,200)+"..."),t}function debugComponent(o,e,n){let l=`${e}<id=${n}>`,r=o=>(!debugSetup.methodBlackList||!debugSetup.methodBlackList.includes(o))&&!(debugSetup.methodWhiteList&&!debugSetup.methodWhiteList.includes(o));r("constructor")&&console.log(`${t} ${l} constructor, props=${toStr(o.props)}`),r("willStart")&&owl.hooks.onWillStart(()=>{console.log(`${t} ${l} willStart`)}),r("mounted")&&owl.hooks.onMounted(()=>{console.log(`${t} ${l} mounted`)}),r("willUpdateProps")&&owl.hooks.onWillUpdateProps(o=>{console.log(`${t} ${l} willUpdateProps, nextprops=${toStr(o)}`)}),r("willPatch")&&owl.hooks.onWillPatch(()=>{console.log(`${t} ${l} willPatch`)}),r("patched")&&owl.hooks.onPatched(()=>{console.log(`${t} ${l} patched`)}),r("willUnmount")&&owl.hooks.onWillUnmount(()=>{console.log(`${t} ${l} willUnmount`)});const s=o.__render.bind(o);o.__render=function(...o){console.log(`${t} ${l} rendering template`),s(...o)};const u=o.render.bind(o);o.render=function(...o){return console.log(`${t} ${l} render`),u(...o)};const c=o.mount.bind(o);o.mount=function(...o){return console.log(`${t} ${l} mount`),c(...o)}}if(Object.defineProperty(owl.Component,"current",{get:()=>o,set(t){o=t;const e=t.constructor.name;if(debugSetup.componentBlackList&&debugSetup.componentBlackList.test(e))return;if(debugSetup.componentWhiteList&&!debugSetup.componentWhiteList.test(e))return;let n;Object.defineProperty(o,"__owl__",{get:()=>n,set(o){debugComponent(t,e,(n=o).id)}})}}),debugSetup.logScheduler){let o;Object.defineProperty(owl.Component.scheduler,"isRunning",{get:()=>o,set(e){e?console.log(`${t} scheduler: start running tasks queue`):console.log(`${t} scheduler: stop running tasks queue`),o=e}})}if(debugSetup.logStore){let o=owl.Store.prototype.dispatch;owl.Store.prototype.dispatch=function(e,...n){return console.log(`${t} store: action '${e}' dispatched. Payload: '${toStr(n)}'`),o.call(this,e,...n)}}}
});
```
Note that it is certainly useful to run this code at some point in an application,
+6 -3
View File
@@ -1,8 +1,11 @@
{
"name": "owl-framework",
"version": "1.0.0-alpha5",
"name": "@odoo/owl",
"version": "1.0.0",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"main": "dist/owl.js",
"types": "dist/types/index.d.ts",
"prepublish": "npm run buildcommonjs",
"files": ["dist/types/", "dist/owl.js"],
"engines": {
"node": ">=10.15.3"
},
+6 -13
View File
@@ -1,25 +1,17 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.0.0-alpha5
- Status: mostly stable
- Current version: 1.0.0
- Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may
change!
### November 2019
Owl will be used in various Odoo projects. We plan to:
- fix any issues encountered
- maybe cleanup slightly the router API
- improve the documentation
- improve error handling, add more helpful error messages
### December 2019
If all goes well, Owl will be upgraded to beta status. From then, no API change,
even small, is expected.
If all goes well, Owl is upgraded to beta status. From now on, no API change,
even small, is expected (but it still could happen).
### End of 2019
@@ -41,7 +33,8 @@ Release v1.0
Maybe:
- reimplement vdom to use *block* system, like Vue 3,
- reimplement vdom to use *block* system, like Vue 3, which should make Owl
much faster
- refactor `QWeb` to use an intermediate representation (some kind of AST) to
allow additional optimisations.
+78 -25
View File
@@ -6,6 +6,7 @@ import "./directive";
import { Fiber } from "./fiber";
import "./props_validation";
import { Scheduler, scheduler } from "./scheduler";
import { activateSheet } from "./styles";
/**
* Owl Component System
@@ -36,6 +37,10 @@ export interface Env {
[key: string]: any;
}
interface MountOptions {
position?: "first-child" | "last-child" | "self";
}
/**
* This is mostly an internal detail of implementation. The Meta interface is
* useful to typecheck and describe the internal keys used by Owl to manage the
@@ -66,10 +71,9 @@ interface Internal<T extends Env, Props> {
parentLastFiberId: number;
// when a rendering is initiated by a parent, it may set variables in 'scope'
// and 'vars' (typically when the component is rendered in a slot). We need to
// (typically when the component is rendered in a slot). We need to
// store that information in case the component would be re-rendered later on.
scope: any;
vars: any;
boundHandlers: { [key: number]: any };
observer: Observer | null;
@@ -84,6 +88,8 @@ interface Internal<T extends Env, Props> {
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
}
export const portalSymbol = Symbol("portal"); // FIXME
//------------------------------------------------------------------------------
// Component
//------------------------------------------------------------------------------
@@ -100,6 +106,7 @@ export class Component<T extends Env, Props extends {}> {
static env: any = {};
// expose scheduler s.t. it can be mocked for testing purposes
static scheduler: Scheduler = scheduler;
__target: HTMLElement | undefined;
/**
* The `el` is the root element of the component. Note that it could be null:
@@ -191,9 +198,11 @@ export class Component<T extends Env, Props extends {}> {
renderFn: qweb.render.bind(qweb, template),
classObj: null,
refs: null,
scope: null,
vars: null
scope: null
};
if (constr.style) {
this.__applyStyles(constr);
}
}
/**
@@ -289,7 +298,8 @@ export class Component<T extends Env, Props extends {}> {
*
* Note that a component can be mounted an unmounted several times
*/
async mount(target: HTMLElement | DocumentFragment): Promise<void> {
async mount(target: HTMLElement | DocumentFragment, options: MountOptions = {}): Promise<void> {
const position = options.position || "last-child";
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
return Promise.resolve();
@@ -299,10 +309,19 @@ export class Component<T extends Env, Props extends {}> {
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message);
}
const fiber = new Fiber(null, this, false, target);
let inserter =
position === "last-child"
? el => target.appendChild(el)
: position === "first-child"
? el => target.prepend(el)
: el => {};
if (position === "self") {
this.__target = target as HTMLElement;
}
const fiber = new Fiber(null, this, false, inserter);
fiber.shouldPatch = false;
if (!__owl__.vnode) {
this.__prepareAndRender(fiber);
this.__prepareAndRender(fiber, () => {});
} else {
this.__render(fiber);
}
@@ -397,14 +416,7 @@ export class Component<T extends Env, Props extends {}> {
* willUnmount().
*/
trigger(eventType: string, payload?: any) {
if (this.el) {
const ev = new OwlEvent(this, eventType, {
bubbles: true,
cancelable: true,
detail: payload
});
this.el.dispatchEvent(ev);
}
this.__trigger(this, eventType, payload);
}
//--------------------------------------------------------------------------
@@ -478,14 +490,30 @@ export class Component<T extends Env, Props extends {}> {
}
}
}
/**
* Private trigger method, allows to choose the component which triggered
* the event in the first place
*/
__trigger(component: Component<any, any>, eventType: string, payload?: any) {
if (this.el) {
const ev = new OwlEvent(component, eventType, {
bubbles: true,
cancelable: true,
detail: payload
});
const triggerHook = this.env[portalSymbol as any];
if (triggerHook) {
triggerHook(ev);
}
this.el.dispatchEvent(ev);
}
}
/**
* The __updateProps method is called by the t-component directive whenever
* it updates a component (so, when the parent template is rerendered).
*/
async __updateProps(nextProps: Props, parentFiber: Fiber, scope: any, vars: any): Promise<void> {
async __updateProps(nextProps: Props, parentFiber: Fiber, scope: any): Promise<void> {
this.__owl__.scope = scope;
this.__owl__.vars = vars;
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const __owl__ = this.__owl__;
@@ -523,18 +551,27 @@ export class Component<T extends Env, Props extends {}> {
*/
__patch(vnode: VNode) {
const __owl__ = this.__owl__;
const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode);
if (this.__target) {
if (this.__target.tagName.toLowerCase() !== vnode.sel) {
throw new Error(
`Cannot attach '${this.constructor.name}' to target node (not same tag name)`
);
}
__owl__.vnode = patch(this.__target, vnode);
delete this.__target;
} else {
const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode);
}
}
/**
* The __prepare method is only called by the t-component directive, when a
* subcomponent is created. It gets its scope and vars, if any, from the
* subcomponent is created. It gets its scope, if any, from the
* parent template.
*/
__prepare(parentFiber: Fiber, scope: any, vars: any) {
__prepare(parentFiber: Fiber, scope: any, cb: CallableFunction): Fiber {
this.__owl__.scope = scope;
this.__owl__.vars = vars;
const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
fiber.shouldPatch = false;
if (!parentFiber.child) {
@@ -543,9 +580,24 @@ export class Component<T extends Env, Props extends {}> {
parentFiber.lastChild!.sibling = fiber;
}
parentFiber.lastChild = fiber;
return this.__prepareAndRender(fiber);
this.__prepareAndRender(fiber, cb);
return fiber;
}
/**
* Apply the stylesheets defined by the component. Note that we need to make
* sure all inherited stylesheets are applied as well. We then delete the
* `style` key from the constructor to make sure we do not apply it again.
*/
private __applyStyles(constr) {
while (constr && constr.style) {
if (constr.hasOwnProperty("style")) {
activateSheet(constr.style, constr.name);
delete constr.style;
}
constr = constr.__proto__;
}
}
__getTemplate(qweb: QWeb): string {
let p = (<any>this).constructor;
if (!p.hasOwnProperty("_template")) {
@@ -565,7 +617,7 @@ export class Component<T extends Env, Props extends {}> {
}
return p._template;
}
async __prepareAndRender(fiber: Fiber) {
async __prepareAndRender(fiber: Fiber, cb: CallableFunction) {
try {
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
} catch (e) {
@@ -577,6 +629,7 @@ export class Component<T extends Env, Props extends {}> {
}
if (!fiber.isCompleted) {
this.__render(fiber);
cb();
}
}
+20 -63
View File
@@ -1,6 +1,6 @@
import { QWeb } from "../qweb/index";
import { INTERP_REGEXP } from "../qweb/compilation_context";
import { MODS_CODE } from "../qweb/extensions";
import { makeHandlerCode, MODS_CODE } from "../qweb/extensions";
//------------------------------------------------------------------------------
// t-component
@@ -189,15 +189,15 @@ QWeb.addDirective({
extraNames: ["props"],
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//COMPONENT");
ctx.rootContext.shouldDefineOwner = true;
ctx.addLine(`// Component '${value}'`);
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true;
ctx.rootContext.shouldDefineScope = true;
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
// t-on- events and t-transition
const events: [string, string[], string, string][] = [];
const events: [string, string][] = [];
let transition: string = "";
const attributes = (<Element>node).attributes;
const props: { [key: string]: string } = {};
@@ -205,19 +205,13 @@ QWeb.addDirective({
const name = attributes[i].name;
const value = attributes[i].textContent!;
if (name.startsWith("t-on-")) {
const [eventName, ...mods] = name.slice(5).split(".");
let extraArgs;
let handlerValue = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
events.push([eventName, mods, handlerValue, extraArgs]);
events.push([name, value]);
} else if (name === "t-transition") {
transition = value;
} else if (!name.startsWith("t-")) {
if (name !== "class" && name !== "style") {
// this is a prop!
props[name] = ctx.formatExpression(value);
props[name] = ctx.formatExpression(value) || "undefined";
}
}
}
@@ -226,7 +220,6 @@ QWeb.addDirective({
let propStr = Object.keys(props)
.map(k => k + ":" + props[k])
.join(",");
let defID = ctx.generateID();
let componentID = ctx.generateID();
const templateKey = ctx.generateTemplateKey();
@@ -285,32 +278,15 @@ QWeb.addDirective({
}
}
let eventsCode = events
.map(function([eventName, mods, handlerValue, extraArgs]) {
let params = "owner";
if (extraArgs) {
if (ctx.loopNumber) {
let argId = ctx.generateID();
// we need to evaluate the arguments now, because the handler will
// be set asynchronously later when the widget is ready, and the
// context might be different.
ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`);
params = `owner, arg${argId}`;
} else {
params = `owner, ${ctx.formatExpression(extraArgs)}`;
}
}
let handler = `function (e) {`;
handler += mods
.map(function(mod) {
return T_COMPONENT_MODS_CODE[mod];
})
.join("");
if (handlerValue) {
handler += `const fn = owner['${handlerValue}'];`;
handler += `if (fn) { fn.call(${params}, e); } else { owner.${handlerValue}; }`;
}
handler += `}`;
return `vn.elm.addEventListener('${eventName}', ${handler});`;
.map(function([name, value]) {
const { event, handler } = makeHandlerCode(
ctx,
name,
value,
false,
T_COMPONENT_MODS_CODE
);
return `vn.elm.addEventListener('${event}', ${handler});`;
})
.join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
@@ -349,25 +325,9 @@ QWeb.addDirective({
}
// SLOTS
const varDefs: string[] = [];
const hasSlots = node.childNodes.length;
if (hasSlots) {
ctx.rootContext.shouldTrackScope = true;
for (let v of Object.values(ctx.variables)) {
if (v["id"]) {
varDefs.push(v["id"]);
}
}
}
let scopeVars;
if (hasSlots) {
let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
scopeVars = `${scope}, ${vars}`;
} else {
scopeVars = "undefined, undefined";
}
let scope = hasSlots ? `Object.assign(Object.create(context), scope)` : "undefined";
ctx.addIf(`w${componentID}`);
@@ -377,8 +337,7 @@ QWeb.addDirective({
styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
}
ctx.addLine(
`w${componentID}.__updateProps(props${componentID}, extra.fiber${scopeVars &&
", " + scopeVars})${styleCode};`
`w${componentID}.__updateProps(props${componentID}, extra.fiber, ${scope})${styleCode};`
);
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
if (registerCode) {
@@ -439,16 +398,14 @@ QWeb.addDirective({
}
}
ctx.addLine(`let def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars});`);
ctx.addLine(
`let fiber = w${componentID}.__prepare(extra.fiber, ${scope}, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
const insertHook = refExpr ? `insert(vn) {${refExpr}},` : "";
ctx.addLine(
`let pvnode = h('dummy', {key: ${templateKey}, hook: {${insertHook}remove() {},destroy(vn) {${finalizeComponentCode}}}});`
);
ctx.addLine(`const fiber = w${componentID}.__owl__.currentFiber;`);
ctx.addLine(
`def${defID}.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
if (registerCode) {
ctx.addLine(registerCode);
}
+39 -55
View File
@@ -46,10 +46,9 @@ export class Fiber {
// scheduler.
counter: number = 0;
target: HTMLElement | null;
inserter: (el: HTMLElement) => void | null;
scope: any;
vars: any;
component: Component<any, any>;
vnode: VNode | null = null;
@@ -62,14 +61,13 @@ export class Fiber {
error?: Error;
constructor(parent: Fiber | null, component: Component<any, any>, force, target) {
constructor(parent: Fiber | null, component: Component<any, any>, force, inserter) {
this.component = component;
this.force = force;
this.target = target;
this.inserter = inserter;
const __owl__ = component.__owl__;
this.scope = __owl__.scope;
this.vars = __owl__.vars;
this.root = parent ? parent.root : this;
this.parent = parent;
@@ -180,9 +178,8 @@ export class Fiber {
*/
complete() {
let component = this.component;
let fiber: Fiber = this;
this.isCompleted = true;
if (!this.target && !component.__owl__.isMounted) {
if (!this.inserter && !component.__owl__.isMounted) {
return;
}
@@ -195,58 +192,49 @@ export class Fiber {
this._walk(doWork);
const patchLen = patchQueue.length;
try {
// call willPatch hook on each fiber of patchQueue
for (let i = 0; i < patchLen; i++) {
fiber = patchQueue[i];
if (fiber.shouldPatch) {
component = fiber.component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.willPatch();
}
}
// call __patch on each fiber of (reversed) patchQueue
for (let i = patchLen - 1; i >= 0; i--) {
fiber = patchQueue[i];
// call willPatch hook on each fiber of patchQueue
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
if (fiber.shouldPatch) {
component = fiber.component;
component.__patch(fiber.vnode!);
if (!fiber.shouldPatch && (!fiber.target || i !== 0)) {
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.__owl__.currentFiber = null;
component.willPatch();
}
}
// insert into the DOM (mount case)
let inDOM = false;
if (this.target) {
this.target.appendChild(this.component.el!);
inDOM = document.body.contains(this.target);
// call __patch on each fiber of (reversed) patchQueue
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode!);
if (!fiber.shouldPatch && (!fiber.inserter || i !== 0)) {
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
}
component.__owl__.currentFiber = null;
}
// call patched/mounted hook on each fiber of (reversed) patchQueue
for (let i = patchLen - 1; i >= 0; i--) {
fiber = patchQueue[i];
component = fiber.component;
if (fiber.shouldPatch && !this.target) {
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
} else if (this.target ? inDOM : true) {
component.__callMounted();
// insert into the DOM (mount case)
let inDOM = false;
if (this.inserter) {
this.inserter(this.component.el!);
inDOM = document.body.contains(this.component.el);
this.component.env.qweb.trigger("dom-appended");
}
// call patched/mounted hook on each fiber of (reversed) patchQueue
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
if (fiber.shouldPatch && !this.inserter) {
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
} else if (this.inserter ? inDOM : true) {
component.__callMounted();
}
} catch (e) {
// if there is no current fiber on component, we are in the situation where
// components were patched to the DOM, but a mounted/patched hook threw an
// error. In that case, we cannot manage the error at a lower level than
// the root fiber, since some components may not have been properly mounted
// patched yet.
const errorFiber = component.__owl__.currentFiber ? fiber : this;
errorFiber.handleError(e);
}
}
@@ -285,11 +273,7 @@ export class Fiber {
qweb.trigger("error", error);
if (canCatch) {
// this.root.isCompleted = false
this.root.isCompleted = false;
// component.__owl__.currentFiber!.root.isCompleted = false;
component.catchError!(error);
} else {
// the 3 next lines aim to mark the root fiber as being in error, and
// to force it to end, without waiting for its children
+18 -8
View File
@@ -25,6 +25,15 @@ export class Scheduler {
this.requestAnimationFrame = requestAnimationFrame;
}
start() {
this.isRunning = true;
this.scheduleTasks();
}
stop() {
this.isRunning = false;
}
addFiber(fiber): Promise<void> {
// if the fiber was remapped into a larger rendering fiber, it may not be a
// root fiber. But we only want to register root fibers
@@ -43,7 +52,7 @@ export class Scheduler {
}
});
if (!this.isRunning) {
this.scheduleTasks();
this.start();
}
});
}
@@ -62,9 +71,10 @@ export class Scheduler {
}
if (task.fiber.counter === 0) {
if (!task.fiber.error) {
task.fiber.complete();
if (!task.fiber.isCompleted) {
return true;
try {
task.fiber.complete();
} catch (e) {
task.fiber.handleError(e);
}
}
task.callback();
@@ -73,16 +83,16 @@ export class Scheduler {
return true;
});
this.tasks = tasks.concat(this.tasks);
if (this.tasks.length === 0) {
this.stop();
}
}
scheduleTasks() {
this.isRunning = true;
this.requestAnimationFrame(() => {
this.flush();
if (this.tasks.length > 0) {
if (this.isRunning) {
this.scheduleTasks();
} else {
this.isRunning = false;
}
});
}
+70
View File
@@ -0,0 +1,70 @@
/**
* Owl Style System
*
* This files contains the Owl code related to processing (extended) css strings
* and creating/adding <style> tags to the document head.
*/
export const STYLESHEETS: { [id: string]: HTMLStyleElement } = {};
export function processSheet(str: string): string {
const tokens = str.split(/(\{|\}|;)/).map(s => s.trim());
const selectorStack: string[][] = [];
const parts: string[] = [];
let rules: string[] = [];
function generateSelector(stackIndex: number, parentSelector?: string) {
const parts: string[] = [];
for (const selector of selectorStack[stackIndex]) {
let part = (parentSelector && parentSelector + " " + selector) || selector;
if (part.includes("&")) {
part = selector.replace(/&/g, parentSelector || "");
}
if (stackIndex < selectorStack.length - 1) {
part = generateSelector(stackIndex + 1, part);
}
parts.push(part);
}
return parts.join(", ");
}
function generateRules() {
if (rules.length) {
parts.push(generateSelector(0) + " {");
parts.push(...rules);
parts.push("}");
rules = [];
}
}
while (tokens.length) {
let token = tokens.shift()!;
if (token === "}") {
generateRules();
selectorStack.pop();
} else {
if (tokens[0] === "{") {
generateRules();
selectorStack.push(token.split(/\s*,\s*/));
tokens.shift();
}
if (tokens[0] === ";") {
rules.push(" " + token + ";");
}
}
}
return parts.join("\n");
}
export function registerSheet(id: string, css: string) {
const sheet = document.createElement("style");
sheet.innerHTML = processSheet(css);
STYLESHEETS[id] = sheet;
}
export function activateSheet(id, name) {
const sheet = STYLESHEETS[id];
if (!sheet) {
throw new Error(
`Invalid css stylesheet for component '${name}'. Did you forget to use the 'css' tag helper?`
);
}
sheet.setAttribute("component", name);
document.head.appendChild(sheet);
}
-1
View File
@@ -20,7 +20,6 @@
export class Observer {
rev: number = 1;
allowMutations: boolean = true;
dirty: boolean = false;
weakMap: WeakMap<any, any> = new WeakMap();
notifyCB() {}
+24
View File
@@ -131,3 +131,27 @@ export function useSubEnv(nextEnv) {
const component = Component.current!;
component.env = Object.assign(Object.create(component.env), nextEnv);
}
// -----------------------------------------------------------------------------
// useExternalListener
// -----------------------------------------------------------------------------
/**
* When a component needs to listen to DOM Events on element(s) that are not
* part of his hierarchy, we can use the `useExternalListener` hook.
* It will correctly add and remove the event listener, whenever the
* component is mounted and unmounted.
*
* Example:
* a menu needs to listen to the click on window to be closed automatically
*
* Usage:
* in the constructor of the OWL component that needs to be notified,
* `useExternalListener(window, 'click', this._doSomething);`
* */
export function useExternalListener(target: HTMLElement, eventName: string, handler, eventParams?) {
const boundHandler = handler.bind(Component.current);
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
}
+2 -1
View File
@@ -12,6 +12,7 @@ import * as _store from "./store";
import * as _utils from "./utils";
import * as _tags from "./tags";
import { AsyncRoot } from "./misc/async_root";
import { Portal } from "./misc/portal";
import * as _hooks from "./hooks";
import * as _context from "./context";
import { Link } from "./router/link";
@@ -29,7 +30,7 @@ export const router = { Router, RouteComponent, Link };
export const Store = _store.Store;
export const utils = _utils;
export const tags = _tags;
export const misc = { AsyncRoot };
export const misc = { AsyncRoot, Portal };
export const hooks = Object.assign({}, _hooks, {
useContext: _context.useContext,
useDispatch: _store.useDispatch,
+167
View File
@@ -0,0 +1,167 @@
import { Component, portalSymbol } from "../component/component";
import { VNode, patch } from "../vdom/index";
import { xml } from "../tags";
import { OwlEvent } from "../core/owl_event";
import { useSubEnv } from "../hooks";
/**
* Portal
*
* The Portal component allows to render a part of a component outside it's DOM.
* It is for example useful for dialogs: for css reasons, dialogs are in general
* placed in a specific spot of the DOM (e.g. directly in the body). With the
* Portal, a component can conditionally specify in its tempate that it contains
* a dialog, and where this dialog should be inserted in the DOM.
*
* The Portal component ensures that the communication between the content of
* the Portal and its parent properly works: business events reaching the Portal
* are re-triggered on an empty <portal> node located in the parent's DOM.
*/
export class Portal extends Component<any, any> {
static template = xml`<portal><t t-slot="default"/></portal>`;
static props = {
target: {
type: String
}
};
// boolean to indicate whether or not we must listen to 'dom-appended' event
// to hook on the moment when the target is inserted into the DOM (because it
// is not when the portal is rendered)
doTargetLookUp: boolean = true;
// set of encountered events that need to be redirected
_handledEvents: Set<string> = new Set();
// function that will be the event's tunnel (needs to be an arrow function to
// avoid having to rebind `this`)
_handlerTunnel: (f: OwlEvent<any>) => void = (ev: OwlEvent<any>) => {
ev.stopPropagation();
this.__trigger(ev.originalComponent, ev.type, ev.detail);
};
// Storing the parent's env
parentEnv: any = null;
// represents the element that is moved somewhere else
portal: VNode | null = null;
// the target where we will move `portal`
target: HTMLElement | null = null;
constructor(parent, props) {
super(parent, props);
this.parentEnv = parent ? parent.env : {};
// put a callback in the env that is propagated to children s.t. portal can
// register an handler to those events just before children will trigger them
useSubEnv({
[portalSymbol]: ev => {
if (!this._handledEvents.has(ev.type)) {
this.portal!.elm!.addEventListener(ev.type, this._handlerTunnel);
this._handledEvents.add(ev.type);
}
}
});
}
/**
* Override to revert back to a classic Component's structure
*
* @override
*/
__callWillUnmount() {
super.__callWillUnmount();
this.el!.appendChild(this.portal!.elm!);
this.doTargetLookUp = true;
}
/**
* At each DOM change, we must ensure that the portal contains exactly one
* child
*/
__checkVNodeStructure(vnode: VNode) {
const children = vnode.children!;
let countRealNodes = 0;
for (let child of children) {
if ((child as VNode).sel) {
countRealNodes++;
}
}
if (countRealNodes !== 1) {
throw new Error(`Portal must have exactly one non-text child (has ${countRealNodes})`);
}
}
/**
* Ensure the target is still there at whichever time we render
*/
__checkTargetPresence() {
if (!this.target || !document.contains(this.target)) {
throw new Error(`Could not find any match for "${this.props.target}"`);
}
}
/**
* Move the portal's element to the target
*/
__deployPortal() {
this.__checkTargetPresence();
this.target!.appendChild(this.portal!.elm!);
}
/**
* Override to remove from the DOM the element we have teleported
*
* @override
*/
__destroy(parent) {
if (this.portal && this.portal.elm) {
const displacedElm = this.portal.elm!;
const parent = displacedElm.parentNode;
if (parent) {
parent.removeChild(displacedElm);
}
}
super.__destroy(parent);
}
/**
* Override to patch the element that has been teleported
*
* @override
*/
__patch(vnode) {
if (this.doTargetLookUp) {
const target = document.querySelector(this.props.target);
if (!target) {
this.env.qweb.on("dom-appended", this, () => {
this.doTargetLookUp = false;
this.env.qweb.off("dom-appended", this);
this.target = document.querySelector(this.props.target);
this.__deployPortal();
});
} else {
this.doTargetLookUp = false;
this.target = target;
}
}
this.__checkVNodeStructure(vnode);
const shouldDeploy =
(!this.portal || this.el!.contains(this.portal.elm!)) && !this.doTargetLookUp;
if (!this.doTargetLookUp && !shouldDeploy) {
// Only on pure patching, provided the
// this.target's parent has not been unmounted
this.__checkTargetPresence();
}
const portalPatch = this.portal ? this.portal : document.createElement(vnode.children[0].sel);
this.portal = patch(portalPatch, vnode.children![0] as VNode);
vnode.children = [];
super.__patch(vnode);
if (shouldDeploy) {
this.__deployPortal();
}
}
/**
* Override to set the env
*/
__trigger(component: Component<any, any>, eventType: string, payload?: any) {
const env = this.env;
this.env = this.parentEnv;
super.__trigger(component, eventType, payload);
this.env = env;
}
}
+152 -114
View File
@@ -1,6 +1,7 @@
import { CompilationContext } from "./compilation_context";
import { QWeb } from "./qweb";
import { htmlToVDOM } from "../vdom/html_to_vdom";
import { QWebVar } from "./expression_parser";
/**
* Owl QWeb Directives
@@ -19,38 +20,40 @@ import { htmlToVDOM } from "../vdom/html_to_vdom";
//------------------------------------------------------------------------------
// t-esc and t-raw
//------------------------------------------------------------------------------
QWeb.utils.getFragment = function(str: string): DocumentFragment {
const temp = document.createElement("template");
temp.innerHTML = str;
return temp.content;
};
QWeb.utils.htmlToVDOM = htmlToVDOM;
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: CompilationContext) {
ctx.rootContext.shouldDefineScope = true;
if (value === "0") {
const caller = ctx.getCaller();
if (caller) {
qweb._compileNode(caller, ctx.getInliningContext());
return;
}
}
if (value.xml instanceof NodeList && !value.id) {
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
if (ctx.parentNode) {
// the 'zero' magical symbol is where we can find the result of the rendering
// of the body of the t-call.
ctx.rootContext.shouldDefineUtils = true;
const zeroArgs = ctx.escaping
? `{text: utils.vDomToString(scope[utils.zero])}`
: `...scope[utils.zero]`;
ctx.addLine(`c${ctx.parentNode}.push(${zeroArgs});`);
}
return;
}
let exprID: string;
if (typeof value === "string") {
exprID = `_${ctx.generateID()}`;
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
ctx.addLine(`let ${exprID} = ${ctx.formatExpression(value)};`);
} else {
exprID = value.id;
exprID = `scope.${value.id}`;
}
ctx.addIf(`${exprID} || ${exprID} === 0`);
ctx.addIf(`${exprID} != null`);
if (ctx.escaping) {
let protectID;
if (value.hasBody) {
protectID = ctx.startProtectScope();
ctx.addLine(
`${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};`
);
}
if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
} else if (ctx.parentNode) {
@@ -59,26 +62,30 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
let nodeID = ctx.generateID();
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`);
if (ctx.rootContext.shouldDefineResult) {
ctx.addLine(`result = vn${nodeID}`);
}
}
if (value.hasBody) {
ctx.stopProtectScope(protectID);
}
} else {
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
if (value.hasBody) {
ctx.addLine(
`const vnodeArray = ${exprID} instanceof utils.VDomArray ? ${exprID} : utils.htmlToVDOM(${exprID});`
);
ctx.addLine(`c${ctx.parentNode}.push(...vnodeArray);`);
} else {
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
}
}
if (node.childNodes.length) {
ctx.addElse();
qweb._compileChildren(node, ctx);
}
if (value.xml instanceof NodeList && value.id) {
ctx.addElse();
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
}
}
ctx.closeIf();
}
@@ -109,24 +116,51 @@ QWeb.addDirective({
name: "set",
extraNames: ["value"],
priority: 60,
atNodeEncounter({ node, ctx }): boolean {
atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldDefineScope = true;
const variable = node.getAttribute("t-set")!;
let value = node.getAttribute("t-value")!;
ctx.variables[variable] = ctx.variables[variable] || {};
ctx.variables[variable] = ctx.variables[variable] || ({} as QWebVar);
let qwebvar = ctx.variables[variable];
const hasBody = node.hasChildNodes();
qwebvar.id = variable;
qwebvar.expr = `scope.${variable}`;
if (value) {
const formattedValue = ctx.formatExpression(value);
if (ctx.variables.hasOwnProperty(variable) && qwebvar.id) {
ctx.addLine(`${qwebvar.id} = ${formattedValue}`);
} else {
const varName = `_${ctx.generateID()}`;
ctx.addLine(`var ${varName} = ${formattedValue};`);
qwebvar.id = varName;
qwebvar.expr = formattedValue;
let scopeExpr = `scope`;
if (ctx.protectedScopeNumber) {
ctx.rootContext.shouldDefineUtils = true;
scopeExpr = `utils.getScope(scope, '${variable}')`;
}
ctx.addLine(`${scopeExpr}.${variable} = ${formattedValue};`);
qwebvar.value = formattedValue;
}
if (hasBody) {
ctx.rootContext.shouldDefineUtils = true;
if (value) {
ctx.addIf(`!(${qwebvar.expr})`);
}
const tempParentNodeID = ctx.generateID();
const _parentNode = ctx.parentNode;
ctx.parentNode = tempParentNodeID;
ctx.addLine(`let c${tempParentNodeID} = new utils.VDomArray();`);
const nodeCopy = node.cloneNode(true) as Element;
for (let attr of ["t-set", "t-value", "t-if", "t-else", "t-elif"]) {
nodeCopy.removeAttribute(attr);
}
qweb._compileNode(nodeCopy, ctx);
ctx.addLine(`${qwebvar.expr} = c${tempParentNodeID}`);
qwebvar.value = `c${tempParentNodeID}`;
qwebvar.hasBody = true;
ctx.parentNode = _parentNode;
if (value) {
ctx.closeIf();
}
} else {
qwebvar.xml = node.childNodes;
}
return true;
}
@@ -140,7 +174,7 @@ QWeb.addDirective({
priority: 20,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-if")!);
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : cond.id!);
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id!}`);
return false;
},
finalize({ ctx }) {
@@ -153,7 +187,9 @@ QWeb.addDirective({
priority: 30,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-elif")!);
ctx.addLine(`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : cond.id}) {`);
ctx.addLine(
`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id}`}) {`
);
ctx.indent();
return false;
},
@@ -182,6 +218,10 @@ QWeb.addDirective({
name: "call",
priority: 50,
atNodeEncounter({ node, qweb, ctx }): boolean {
// Step 1: sanity checks
// ------------------------------------------------
ctx.rootContext.shouldDefineScope = true;
ctx.rootContext.shouldDefineUtils = true;
if (node.nodeName !== "t") {
throw new Error("Invalid tag for t-call directive (should be 't')");
}
@@ -190,77 +230,65 @@ QWeb.addDirective({
if (!nodeTemplate) {
throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
}
const nodeCopy = node.cloneNode(true) as Element;
nodeCopy.removeAttribute("t-call");
// extract variables from nodecopy
const tempCtx = new CompilationContext();
tempCtx.allowMultipleRoots = true;
qweb._compileNode(nodeCopy, tempCtx);
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
const templateMap = Object.create(ctx.templates);
// open new scope, if necessary
const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
// compile sub template
let subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
subCtx = subCtx.subContext("templates", templateMap);
if (templateMap[subTemplate]) {
// OUCH, IT IS A RECURSIVE TEMPLATE SITUATION...
// This is a tricky situation... We obviously cannot inline the compiled
// template. So, what we need to do is to compile it, and make sure we
// properly transfer everything from the current scope to the sub template.
ctx.rootContext.shouldTrackScope = true;
ctx.rootContext.shouldDefineOwner = true;
let subTemplateName;
if (ctx.hasParentWidget) {
subTemplateName = ctx.templateName;
} else {
subTemplateName = `__${ctx.generateID()}`;
subCtx.variables = {};
let id = 0;
for (let v in vars) {
subCtx.variables[v] = vars[v];
(vars[v] as any).id = `_v${id++}`;
}
const subTemplateFn = qweb._compile(subTemplateName, nodeTemplate.elem, subCtx);
qweb.recursiveFns[subTemplateName] = subTemplateFn;
}
let varCode = `{}`;
if (Object.keys(vars).length) {
let id = 0;
const content = Object.values(vars)
.map((v: any) => `_v${id++}: ${v.expr}`)
.join(",");
varCode = `{${content}}`;
}
ctx.addLine(
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`
);
return true;
// Step 2: compile target template in sub templates
// ------------------------------------------------
if (!qweb.subTemplates[subTemplate]) {
qweb.subTemplates[subTemplate] = true;
const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx, true);
qweb.subTemplates[subTemplate] = subTemplateFn;
}
templateMap[subTemplate] = true;
if (hasNewVariables) {
ctx.addLine("{");
// Step 3: compile t-call body if necessary
// ------------------------------------------------
let hasBody = node.hasChildNodes();
let protectID;
if (hasBody) {
// we add a sub scope to protect the ambient scope
ctx.addLine(`{`);
ctx.indent();
// add new variables, if any
for (let key in tempCtx.variables) {
const v = tempCtx.variables[key];
if (v.expr) {
ctx.addLine(`let ${v.id} = ${v.expr};`);
}
// todo: handle XML variables...
protectID = ctx.startProtectScope();
const nodeCopy = node.cloneNode(true) as Element;
for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) {
nodeCopy.removeAttribute(attr);
}
}
qweb._compileNode(nodeTemplate.elem, subCtx);
// close new scope
if (hasNewVariables) {
const parentNode = ctx.parentNode;
ctx.parentNode = "__0";
// this local scope is intended to trap c__0
ctx.addLine(`{`);
ctx.indent();
ctx.addLine("let c__0 = [];");
qweb._compileNode(nodeCopy, ctx);
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine("scope[utils.zero] = c__0;");
ctx.parentNode = parentNode;
ctx.dedent();
ctx.addLine("}");
ctx.addLine(`}`);
}
// Step 4: add the appropriate function call to current component
// ------------------------------------------------
const callingScope = hasBody ? "scope" : "Object.assign(Object.create(context), scope)";
const parentComponent = `utils.getComponent(context)`;
const key = ctx.generateTemplateKey();
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`;
if (ctx.parentNode) {
ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, ${extra});`);
} else {
// this is a t-call with no parentnode, we need to extract the result
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`result = []`);
ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, ${extra});`);
ctx.addLine(`result = result[0]`);
}
// Step 5: restore previous scope
// ------------------------------------------------
if (hasBody) {
ctx.stopProtectScope(protectID);
ctx.dedent();
ctx.addLine(`}`);
}
return true;
@@ -275,29 +303,31 @@ QWeb.addDirective({
extraNames: ["as"],
priority: 10,
atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldProtectContext = true;
ctx.rootContext.shouldDefineScope = true;
ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1);
const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID();
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine(`let _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
let keysID = ctx.generateID();
let valuesID = ctx.generateID();
ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
ctx.addLine(`let _${keysID} = _${valuesID} = _${arrayID};`);
ctx.addIf(`!(_${arrayID} instanceof Array)`);
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf();
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
ctx.addLine(`let _length${keysID} = _${keysID}.length;`);
let varsID = ctx.startProtectScope(true);
const loopVar = `i${ctx.loopNumber}`;
ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
ctx.indent();
ctx.addToScope(name + "_first", `${loopVar} === 0`);
ctx.addToScope(name + "_last", `${loopVar} === _length${keysID} - 1`);
ctx.addToScope(name + "_index", loopVar);
ctx.addToScope(name, `_${keysID}[${loopVar}]`);
ctx.addToScope(name + "_value", `_${valuesID}[${loopVar}]`);
ctx.addLine(`scope.${name}_first = ${loopVar} === 0`);
ctx.addLine(`scope.${name}_last = ${loopVar} === _length${keysID} - 1`);
ctx.addLine(`scope.${name}_index = ${loopVar}`);
ctx.addLine(`scope.${name} = _${keysID}[${loopVar}]`);
ctx.addLine(`scope.${name}_value = _${valuesID}[${loopVar}]`);
const nodeCopy = <Element>node.cloneNode(true);
let shouldWarn =
!nodeCopy.hasAttribute("t-key") &&
@@ -309,11 +339,19 @@ QWeb.addDirective({
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
);
}
if (nodeCopy.hasAttribute("t-key")) {
const expr = ctx.formatExpression(nodeCopy.getAttribute("t-key")!);
ctx.addLine(`let key${ctx.loopNumber} = ${expr};`);
nodeCopy.removeAttribute("t-key");
} else {
ctx.addLine(`let key${ctx.loopNumber} = i${ctx.loopNumber};`);
}
nodeCopy.removeAttribute("t-foreach");
qweb._compileNode(nodeCopy, ctx);
ctx.dedent();
ctx.addLine("}");
ctx.stopProtectScope(varsID);
return true;
}
});
+55 -73
View File
@@ -1,4 +1,4 @@
import { compileExpr, QWebVar } from "./expression_parser";
import { compileExpr, compileExprToArray, QWebVar } from "./expression_parser";
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------
@@ -10,36 +10,30 @@ export class CompilationContext {
code: string[] = [];
variables: { [key: string]: QWebVar } = {};
escaping: boolean = false;
parentNode: number | null = null;
parentNode: number | null | string = null;
parentTextNode: number | null = null;
rootNode: number | null = null;
indentLevel: number = 0;
rootContext: CompilationContext;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
shouldDefineParent: boolean = false;
shouldDefineScope: boolean = false;
protectedScopeNumber: number = 0;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldDefineRefs: boolean = false;
shouldDefineResult: boolean = true;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
loopNumber: number = 0;
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false;
scopeVars: any[] = [];
currentKey: string = "";
templates: { [key: string]: boolean } = {};
callingLevel: number = 0;
inliningLevel: number = 0;
hasKey0: boolean = false;
keyStack: boolean[] = [];
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
this.templates[this.templateName] = true;
this.addLine("var h = this.h;");
this.addLine("let h = this.h;");
}
generateID(): number {
@@ -54,47 +48,31 @@ export class CompilationContext {
* Such a key is necessary when we need to associate an id to some element
* generated by a template (for example, a component)
*/
generateTemplateKey(): string {
generateTemplateKey(prefix: string = ""): string {
const id = this.generateID();
let locationExpr = `\`__${this.generateID()}__`;
for (let i = 0; i < this.loopNumber - 1; i++) {
locationExpr += `\${i${i + 1}}__`;
if (this.loopNumber === 0 && !this.hasKey0) {
return `'${prefix}__${id}__'`;
}
if (this.currentKey) {
const k = this.currentKey;
this.addLine(`let k${id} = ${locationExpr}\` + ${k};`);
} else {
locationExpr += this.loopNumber ? `\${i${this.loopNumber}}__\`` : "`";
this.addLine(`let k${id} = ${locationExpr};`);
let key = `\`${prefix}__${id}__`;
let start = this.hasKey0 ? 0 : 1;
for (let i = start; i < this.loopNumber + 1; i++) {
key += `\${key${i}}__`;
}
this.addLine(`let k${id} = ${key}\`;`);
return `k${id}`;
}
generateCode(): string[] {
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
if (shouldTrackScope) {
// add some vars to scope if needed
for (let scopeVar of this.scopeVars.reverse()) {
let { index, key, indent } = scopeVar;
const prefix = new Array(indent + 2).join(" ");
this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
}
this.code.unshift(" const scope = Object.create(null);");
}
if (this.shouldProtectContext) {
this.code.unshift(" context = Object.create(context);");
}
if (this.shouldDefineResult) {
this.code.unshift(" let result;");
}
if (this.shouldDefineScope) {
this.code.unshift(" let scope = Object.create(context);");
}
if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
}
if (this.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
this.code.unshift(" let owner = context;");
}
if (this.shouldDefineParent) {
if (this.hasParentWidget) {
this.code.unshift(" let parent = extra.parent;");
@@ -131,19 +109,15 @@ export class CompilationContext {
subContext(key: keyof CompilationContext, value: any): CompilationContext {
const newContext = Object.create(this);
newContext[key] = value;
if (key === "caller") {
newContext.callingLevel++;
newContext.inliningLevel++;
}
return newContext;
}
indent() {
this.indentLevel++;
this.rootContext.indentLevel++;
}
dedent() {
this.indentLevel--;
this.rootContext.indentLevel--;
}
addLine(line: string): number {
@@ -152,11 +126,6 @@ export class CompilationContext {
return this.code.length - 1;
}
addToScope(key: string, expr: string) {
const index = this.addLine(`context.${key} = ${expr};`);
this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel });
}
addIf(condition: string) {
this.addLine(`if (${condition}) {`);
this.indent();
@@ -172,27 +141,6 @@ export class CompilationContext {
this.dedent();
this.addLine("}");
}
/**
* Recursively (inverse) fetches the `caller` of a context
* Useful to determine to which t-call a t-raw="0" refers
*/
getCaller(targetLevel?: number): Element | null {
if (targetLevel === undefined) {
targetLevel = this.inliningLevel;
}
if (targetLevel === this.callingLevel) {
return this.caller || null;
}
const proto = (this as any).__proto__;
return proto ? proto.getCaller(targetLevel) : null;
}
/**
* Marks the context with the current recursive level
* in which we are for inlining archs (t-raw="0")
*/
getInliningContext(): CompilationContext {
return this.subContext("inliningLevel", this.inliningLevel - 1);
}
getValue(val: any): QWebVar | string {
return val in this.variables ? this.getValue(this.variables[val]) : val;
@@ -205,8 +153,27 @@ export class CompilationContext {
* - replace already defined variables by their internal name
*/
formatExpression(expr: string): string {
this.rootContext.shouldDefineScope = true;
return compileExpr(expr, this.variables);
}
captureExpression(expr: string): string {
this.rootContext.shouldDefineScope = true;
const argId = this.generateID();
const tokens = compileExprToArray(expr, this.variables);
const done = new Set();
return tokens
.map(tok => {
if (tok.varName) {
if (!done.has(tok.varName)) {
done.add(tok.varName);
this.addLine(`const ${tok.varName}_${argId} = ${tok.value};`);
}
tok.value = `${tok.varName}_${argId}`;
}
return tok.value;
})
.join("");
}
/**
* Perform string interpolation on the given string. Note that if the whole
@@ -225,4 +192,19 @@ export class CompilationContext {
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`";
}
startProtectScope(codeBlock?: boolean): number {
const protectID = this.generateID();
this.rootContext.protectedScopeNumber++;
this.rootContext.shouldDefineScope = true;
const scopeExpr = codeBlock
? `Object.create(scope);`
: `Object.assign(Object.create(context), scope);`;
this.addLine(`let _origScope${protectID} = scope;`);
this.addLine(`scope = ${scopeExpr}`);
return protectID;
}
stopProtectScope(protectID: number) {
this.rootContext.protectedScopeNumber--;
this.addLine(`scope = _origScope${protectID};`);
}
}
+45 -17
View File
@@ -39,9 +39,10 @@ const WORD_REPLACEMENT = {
};
export interface QWebVar {
id?: string;
expr?: string;
xml?: NodeList;
id: string; // foo
expr: string; // scope.foo (local variables => only foo)
value?: string; // 1 + 3
hasBody?: boolean;
}
//------------------------------------------------------------------------------
@@ -63,7 +64,9 @@ type TKind =
interface Token {
type: TKind;
value: string;
originalValue?: string;
size?: number;
varName?: string;
}
const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
@@ -79,7 +82,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
// note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ".split(",");
const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ".split(",");
type Tokenizer = (expr: string) => Token | false;
@@ -227,35 +230,60 @@ export function tokenize(expr: string): Token[] {
* - unless the previous token is a dot (in that case, this is a property: `a.b`)
* - or if the previous token is a left brace or a comma, and the next token is
* a colon (in that case, this is an object key: `{a: b}`)
*
* Some specific code is also required to support arrow functions. If we detect
* the arrow operator, then we add the current (or some previous tokens) token to
* the list of variables so it does not get replaced by a lookup in the context
*/
export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): string {
export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar }): Token[] {
scope = Object.create(scope);
const tokens = tokenize(expr);
let result = "";
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
let prevToken = tokens[i - 1];
let nextToken = tokens[i + 1];
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
// we need to find if it is a variable
let isVar = true;
let prevToken = tokens[i - 1];
if (prevToken) {
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false;
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
let nextToken = tokens[i + 1];
if (nextToken && nextToken.type === "COLON") {
isVar = false;
}
}
}
if (isVar) {
if (token.value in vars && "id" in vars[token.value]) {
token.value = vars[token.value].id!;
} else {
token.value = `context['${token.value}']`;
}
if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
if (token.type === "RIGHT_PAREN") {
let j = i - 1;
while (j > 0 && tokens[j].type !== "LEFT_PAREN") {
if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) {
tokens[j].value = tokens[j].originalValue!;
scope[tokens[j].value] = { id: tokens[j].value, expr: tokens[j].value };
}
j--;
}
} else {
scope[token.value] = { id: token.value, expr: token.value };
}
}
if (isVar) {
token.varName = token.value;
if (token.value in scope && "id" in scope[token.value]) {
token.value = scope[token.value].expr!;
} else {
token.originalValue = token.value;
token.value = `scope['${token.value}']`;
}
}
result += token.value;
}
return result;
return tokens;
}
export function compileExpr(expr: string, scope: { [key: string]: QWebVar }): string {
return compileExprToArray(expr, scope)
.map(t => t.value)
.join("");
}
+80 -46
View File
@@ -26,49 +26,67 @@ export const MODS_CODE = {
stop: "e.stopPropagation();"
};
interface HandlerInfo {
event: string;
handler: string;
}
const FNAMEREGEXP = /^[$A-Z_][0-9A-Z_$]*$/i;
export function makeHandlerCode(
ctx,
fullName,
value,
putInCache: boolean,
modcodes = MODS_CODE
): HandlerInfo {
const [event, ...mods] = fullName.slice(5).split(".");
if (!event) {
throw new Error("Missing event name with t-on directive");
}
let code: string;
// check if it is a method with no args, a method with args or an expression
let args: string = "";
const name: string = value.replace(/\(.*\)/, function(_args) {
args = _args.slice(1, -1);
return "";
});
const isMethodCall = name.match(FNAMEREGEXP);
// then generate code
if (isMethodCall) {
ctx.rootContext.shouldDefineUtils = true;
const comp = `utils.getComponent(context)`;
if (args) {
const argId = ctx.generateID();
ctx.addLine(`let args${argId} = [${ctx.formatExpression(args)}];`);
code = `${comp}['${name}'](...args${argId}, e);`;
putInCache = false;
} else {
code = `${comp}['${name}'](e);`;
}
} else {
// if we get here, then it is an expression
// we need to capture every variable in it
putInCache = false;
code = ctx.captureExpression(value);
}
const modCode = mods.map(mod => modcodes[mod]).join("");
let handler = `function (e) {if (!context.__owl__.isMounted){return}${modCode}${code}}`;
if (putInCache) {
const key = ctx.generateTemplateKey(event);
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || ${handler};`);
handler = `extra.handlers[${key}]`;
}
return { event, handler };
}
QWeb.addDirective({
name: "on",
priority: 90,
atNodeCreation({ ctx, fullName, value, nodeID }) {
ctx.rootContext.shouldDefineOwner = true;
const [eventName, ...mods] = fullName.slice(5).split(".");
if (!eventName) {
throw new Error("Missing event name with t-on directive");
}
let extraArgs;
let handlerName = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
let handler = `function (e) {`;
handler += mods
.map(function(mod) {
return MODS_CODE[mod];
})
.join("");
if (handlerName) {
if (!extraArgs) {
handler += `const fn = context['${handlerName}'];`;
handler += `if (fn) { fn.call(${params}, e); } else { context.${handlerName}; }`;
handler += `}`;
ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
);
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
} else {
const handlerKey = `handler${ctx.generateID()}`;
ctx.addLine(
`const ${handlerKey} = context['${handlerName}'] && context['${handlerName}'].bind(${params});`
);
handler += `if (${handlerKey}) { ${handlerKey}(e); } else { context.${value}; }`;
handler += `}`;
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
}
} else {
handler += "}";
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
}
const { event, handler } = makeHandlerCode(ctx, fullName, value, true);
ctx.addLine(`p${nodeID}.on['${event}'] = ${handler};`);
}
});
@@ -195,9 +213,8 @@ QWeb.addDirective({
QWeb.addDirective({
name: "slot",
priority: 80,
atNodeEncounter({ ctx, value }): boolean {
atNodeEncounter({ ctx, value, node, qweb }): boolean {
const slotKey = ctx.generateID();
ctx.rootContext.shouldDefineOwner = true;
ctx.addLine(
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
);
@@ -211,11 +228,17 @@ QWeb.addDirective({
ctx.addLine(`result = {}`);
}
ctx.addLine(
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: extra.parent || owner}));`
`slot${slotKey}.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: ${parentNode}, parent: extra.parent || context}));`
);
if (!ctx.parentNode) {
ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`);
}
if (node.hasChildNodes()) {
ctx.addElse();
const nodeCopy = <Element>node.cloneNode(true);
nodeCopy.removeAttribute("t-slot");
qweb._compileNode(nodeCopy, ctx);
}
ctx.closeIf();
return true;
}
@@ -281,9 +304,20 @@ QWeb.addDirective({
QWeb.addDirective({
name: "key",
priority: 45,
atNodeEncounter({ ctx, value }) {
let id = ctx.generateID();
ctx.addLine(`const nodeKey${id} = ${ctx.formatExpression(value)};`);
ctx.currentKey = `nodeKey${id}`;
atNodeEncounter({ ctx, value, node }) {
if (ctx.loopNumber === 0) {
ctx.keyStack.push(ctx.rootContext.hasKey0);
ctx.rootContext.hasKey0 = true;
}
ctx.addLine("{");
ctx.indent();
ctx.addLine(`let key${ctx.loopNumber} = ${ctx.formatExpression(value)};`);
},
finalize({ ctx }) {
ctx.dedent();
ctx.addLine("}");
if (ctx.loopNumber === 0) {
ctx.rootContext.hasKey0 = ctx.keyStack.pop() as boolean;
}
}
});
+83 -39
View File
@@ -1,7 +1,7 @@
import { EventBus } from "../core/event_bus";
import { h, patch, VNode } from "../vdom/index";
import { CompilationContext } from "./compilation_context";
import { shallowEqual } from "../utils";
import { shallowEqual, escape } from "../utils";
import { addNS } from "../vdom/vdom";
/**
@@ -86,7 +86,12 @@ interface Utils {
[key: string]: any;
}
function isComponent(obj) {
return obj && obj.hasOwnProperty("__owl__");
}
const UTILS: Utils = {
zero: Symbol("zero"),
toObj(expr) {
if (typeof expr === "string") {
expr = expr.trim();
@@ -105,6 +110,37 @@ const UTILS: Utils = {
shallowEqual,
addNameSpace(vnode) {
addNS(vnode.data, vnode.children, vnode.sel);
},
VDomArray: class VDomArray extends Array {},
vDomToString: function(vdom: VNode[]): string {
return vdom
.map(vnode => {
if (vnode.sel) {
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
} else {
return vnode.text;
}
})
.join("");
},
getComponent(obj) {
while (obj && !isComponent(obj)) {
obj = obj.__proto__;
}
return obj;
},
getScope(obj, property: string) {
const obj0 = obj;
while (obj && !obj.hasOwnProperty(property)) {
const newObj = obj.__proto__;
if (!newObj || isComponent(newObj)) {
return obj0;
}
obj = newObj;
}
return obj;
}
};
@@ -171,7 +207,7 @@ export class QWeb extends EventBus {
// recursiveTemplates contains sub templates called with t-call, but which
// ends up in recursive situations. This is very similar to the slot situation,
// as in we need to propagate the scope.
recursiveFns = {};
subTemplates = {};
isUpdating: boolean = false;
translateFn?: QWebConfig["translateFn"];
@@ -333,8 +369,17 @@ export class QWeb extends EventBus {
return vnode.text!;
}
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
const elem = patch(node, vnode).elm as HTMLElement;
function escapeTextNodes(node) {
if (node.nodeType === 3) {
node.textContent = escape(node.textContent);
}
for (let n of node.childNodes) {
escapeTextNodes(n);
}
}
escapeTextNodes(elem);
return elem.outerHTML;
}
/**
@@ -353,31 +398,29 @@ export class QWeb extends EventBus {
});
}
_compile(name: string, elem: Element, parentContext?: CompilationContext): CompiledTemplate {
_compile(
name: string,
elem: Element,
parentContext?: CompilationContext,
defineKey?: boolean
): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new CompilationContext(name);
if (elem.tagName !== "t") {
ctx.shouldDefineResult = false;
}
if (parentContext) {
ctx.templates = Object.create(parentContext.templates);
ctx.variables = Object.create(parentContext.variables);
ctx.parentNode = parentContext.parentNode || ctx.generateID();
ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true;
ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
for (let v in parentContext.variables) {
let variable = <any>parentContext.variables[v];
if (variable.id) {
ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`);
}
if (defineKey) {
ctx.addLine(`let key0 = extra.key || "";`);
ctx.hasKey0 = true;
}
}
if (parentContext) {
ctx.addLine(" Object.assign(context, extra.fiber.scope);");
}
this._compileNode(elem, ctx);
if (!parentContext) {
@@ -392,12 +435,13 @@ export class QWeb extends EventBus {
}
let code = ctx.generateCode();
const templateName = ctx.templateName.replace(/`/g, "'").slice(0, 200);
code.unshift(` // Template name: "${templateName}"`);
let template;
try {
template = new Function("context", "extra", code.join("\n")) as CompiledTemplate;
template = new Function("context, extra", code.join("\n")) as CompiledTemplate;
} catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(code.join("\n"));
console.groupEnd();
@@ -446,7 +490,7 @@ export class QWeb extends EventBus {
// this is an unusual situation: this text node is the result of the
// template rendering.
let nodeID = ctx.generateID();
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
ctx.addLine(`let vn${nodeID} = {text: \`${text}\`};`);
ctx.addLine(`result = vn${nodeID};`);
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
@@ -454,10 +498,6 @@ export class QWeb extends EventBus {
return;
}
if (ctx !== ctx.rootContext) {
ctx = ctx.subContext("currentKey", ctx.currentKey);
}
const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) {
// this is a component, we modify in place the xml document to change
@@ -650,15 +690,16 @@ export class QWeb extends EventBus {
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
const attID = ctx.generateID();
if (name === "class") {
let classDef = value
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
if ((value = value.trim())) {
let classDef = value
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
} else {
ctx.addLine(`var _${attID} = '${value}';`);
ctx.addLine(`let _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
@@ -672,7 +713,7 @@ export class QWeb extends EventBus {
if (name.startsWith("t-att-")) {
let attName = name.slice(6);
const v = ctx.getValue(value);
let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : v.id;
let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : `scope.${v.id}`;
if (attName === "class") {
ctx.rootContext.shouldDefineUtils = true;
@@ -694,12 +735,12 @@ export class QWeb extends EventBus {
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
ctx.addLine(`let _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1);
}
ctx.addLine(`var _${attID} = ${formattedValue};`);
ctx.addLine(`let _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
}
@@ -715,9 +756,9 @@ export class QWeb extends EventBus {
const attID = ctx.generateID();
let staticVal = (<Element>node).getAttribute(attName);
if (staticVal) {
ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
ctx.addLine(`let _${attID} = '${staticVal} ' + ${formattedExpr};`);
} else {
ctx.addLine(`var _${attID} = ${formattedExpr};`);
ctx.addLine(`let _${attID} = ${formattedExpr};`);
}
attrs.push(`${attName}: _${attID}`);
}
@@ -725,13 +766,13 @@ export class QWeb extends EventBus {
// t-att= attributes
if (name === "t-att") {
let id = ctx.generateID();
ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
ctx.addLine(`let _${id} = ${ctx.formatExpression(value!)};`);
tattrs.push(id);
}
}
let nodeID = ctx.generateID();
let nodeKey = ctx.currentKey || nodeID;
const parts = [`key:${nodeKey}`];
let key = ctx.loopNumber || ctx.hasKey0 ? `\`\${key${ctx.loopNumber}}_${nodeID}\`` : nodeID;
const parts = [`key:${key}`];
if (attrs.length + tattrs.length > 0) {
parts.push(`attrs:{${attrs.join(",")}}`);
}
@@ -757,9 +798,12 @@ export class QWeb extends EventBus {
ctx.addLine(`}`);
ctx.closeIf();
}
ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
ctx.addLine(`let vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
} else if (ctx.loopNumber || ctx.hasKey0) {
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`result = vn${nodeID};`);
}
return nodeID;
+4 -1
View File
@@ -132,7 +132,7 @@ export function useStore(selector, options: SelectorOptions = {}): any {
__destroy.call(component, parent);
};
if (typeof result !== "object") {
if (typeof result !== "object" || result === null) {
return result;
}
return new Proxy(result, {
@@ -141,6 +141,9 @@ export function useStore(selector, options: SelectorOptions = {}): any {
},
set(target, k, v) {
throw new Error("Store state should only be modified through actions");
},
has(target, k) {
return k in result;
}
});
}
+17
View File
@@ -1,4 +1,5 @@
import { QWeb } from "./qweb/index";
import { registerSheet } from "./component/styles";
/**
* Owl Tags
@@ -25,3 +26,19 @@ export function xml(strings, ...args) {
QWeb.registerTemplate(name, value);
return name;
}
/**
* CSS tag helper for defining inline stylesheets. With this, one can simply define
* an inline stylesheet with just the following code:
* ```js
* class A extends Component {
* static style = css`.component-a { color: red; }`;
* }
* ```
*/
export function css(strings, ...args) {
const name = `__sheet__${QWeb.nextId++}`;
const value = String.raw(strings, ...args);
registerSheet(name, value);
return name;
}
+3 -6
View File
@@ -58,12 +58,9 @@ export function escape(str: string | number | undefined): string {
if (typeof str === "number") {
return String(str);
}
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&#x27;")
.replace(/`/g, "&#x60;");
const p = document.createElement("p");
p.textContent = str;
return p.innerHTML;
}
/**
+1 -1
View File
@@ -219,7 +219,7 @@ function updateClass(oldVnode: VNode, vnode: VNode): void {
elm = vnode.elm as Element;
for (name in oldClass) {
if (!klass[name]) {
if (name && !klass[name]) {
elm.classList.remove(name);
}
}
+91 -95
View File
@@ -1,154 +1,149 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`animations t-transition combined with component 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"Parent\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.h;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//COMPONENT
let k4 = \`__5__\`;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
let vn1 = h('div', p1, c1);
// Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined);
let pvnode = w3.__owl__.pvnode;
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
const __patch3 = w3.__patch;
w3.__patch = fiber => {__patch3.call(w3, fiber); if(!w3.__owl__.transitionInserted){w3.__owl__.transitionInserted = true;utils.transitionInsert(w3.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k4] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined);
let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {let finalize = () => {
w3.destroy();
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy();
};
delete w3.__owl__.transitionInserted;
delete w2.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
w2.__owl__.pvnode = pvnode;
}
w3.__owl__.parentLastFiberId = extra.fiber.id;
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`animations t-transition combined with t-component and t-if 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"Parent\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.h;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].display) {
//COMPONENT
let k4 = \`__5__\`;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
let vn1 = h('div', p1, c1);
if (scope['state'].display) {
// Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined);
let pvnode = w3.__owl__.pvnode;
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
const __patch3 = w3.__patch;
w3.__patch = fiber => {__patch3.call(w3, fiber); if(!w3.__owl__.transitionInserted){w3.__owl__.transitionInserted = true;utils.transitionInsert(w3.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k4] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined);
let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {let finalize = () => {
w3.destroy();
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy();
};
delete w3.__owl__.transitionInserted;
delete w2.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
w2.__owl__.pvnode = pvnode;
}
w3.__owl__.parentLastFiberId = extra.fiber.id;
w2.__owl__.parentLastFiberId = extra.fiber.id;
}
return vn1;
}"
`;
exports[`animations t-transition combined with t-component, remove and re-add before transitionend 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.h;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].flag) {
//COMPONENT
let k4 = \`__5__\`;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
let vn1 = h('div', p1, c1);
if (scope['state'].flag) {
// Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined);
let pvnode = w3.__owl__.pvnode;
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
const __patch3 = w3.__patch;
w3.__patch = fiber => {__patch3.call(w3, fiber); if(!w3.__owl__.transitionInserted){w3.__owl__.transitionInserted = true;utils.transitionInsert(w3.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k4] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined);
let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {let finalize = () => {
w3.destroy();
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy();
};
delete w3.__owl__.transitionInserted;
delete w2.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
w2.__owl__.pvnode = pvnode;
}
w3.__owl__.parentLastFiberId = extra.fiber.id;
w2.__owl__.parentLastFiberId = extra.fiber.id;
}
return vn1;
}"
`;
exports[`animations t-transition with no delay/duration 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
var h = this.h;
let h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
let vn1 = h('span', p1, c1);
p1.hook = {
insert: vn => {
utils.transitionInsert(vn, 'jupiler');
@@ -163,12 +158,13 @@ exports[`animations t-transition with no delay/duration 1`] = `
`;
exports[`animations t-transition, on a simple node (insert) 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
var h = this.h;
let h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
let vn1 = h('span', p1, c1);
p1.hook = {
insert: vn => {
utils.transitionInsert(vn, 'chimay');
+6 -6
View File
@@ -254,11 +254,11 @@ describe("animations", () => {
widget.state.display = false;
patchNextFrame(cb => {
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="__5__">blue</span></div>'
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="__3__">blue</span></div>'
);
cb();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__5__">blue</span></div>'
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>'
);
def.resolve();
});
@@ -371,7 +371,7 @@ describe("animations", () => {
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__5__">blue</span></div>');
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__3__">blue</span></div>');
});
test("transitionInsert is called the correct amount of times", async () => {
@@ -405,14 +405,14 @@ describe("animations", () => {
widget.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__5__">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);
widget.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__5__">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);
@@ -423,7 +423,7 @@ describe("animations", () => {
expect(QWeb.utils.transitionInsert).toBeCalledTimes(3);
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__5__">blue</span></div>');
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__3__">blue</span></div>');
QWeb.utils.transitionInsert = oldTransitionInsert;
});
});
File diff suppressed because it is too large Load Diff
@@ -1,41 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`props validation props are validated in dev mode (code snapshot) 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"App\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.h;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//COMPONENT
let k4 = \`__5__\`;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let props3 = {message:1};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
let vn1 = h('div', p1, c1);
// Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {message:1};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined);
let pvnode = w3.__owl__.pvnode;
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[k4] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined);
let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {w3.destroy();}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
w2.__owl__.pvnode = pvnode;
}
w3.__owl__.parentLastFiberId = extra.fiber.id;
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
@@ -0,0 +1,488 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`t-slot directive can define and call slots 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"Parent\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Dialog'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Dialog\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Dialog'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`t-slot directive can define and call slots 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"Dialog\\"
let h = this.h;
let c6 = [], p6 = {key:6};
let vn6 = h('div', p6, c6);
let c7 = [], p7 = {key:7};
let vn7 = h('div', p7, c7);
c6.push(vn7);
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot8) {
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context}));
}
let c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9);
c6.push(vn9);
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot10) {
slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
}
return vn6;
}"
`;
exports[`t-slot directive can define and call slots 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_header_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`header\`});
}"
`;
exports[`t-slot directive can define and call slots 4`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`footer\`});
}"
`;
exports[`t-slot directive content is the default slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts rocks\`});
}"
`;
exports[`t-slot directive dafault slots can define a default content 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let h = this.h;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot5) {
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context}));
} else {
c4.push({text: \`default content\`});
}
return vn4;
}"
`;
exports[`t-slot directive default slot work with text nodes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let h = this.h;
let c1 = extra.parentNode;
c1.push({text: \`sts rocks\`});
}"
`;
exports[`t-slot directive multiple roots are allowed in a default slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts\`});
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`rocks\`});
}"
`;
exports[`t-slot directive multiple roots are allowed in a named slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_content_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts\`});
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`rocks\`});
}"
`;
exports[`t-slot directive named slots can define a default content 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let h = this.h;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot5) {
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context}));
} else {
c4.push({text: \`default content\`});
}
return vn4;
}"
`;
exports[`t-slot directive refs are properly bound in slots 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils;
context.__owl__.refs = context.__owl__.refs || {};
let h = this.h;
let c1 = extra.parentNode;
let c8 = [], p8 = {key:8,on:{}};
let vn8 = h('button', p8, c8);
c1.push(vn8);
extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p8.on['click'] = extra.handlers['click__9__'];
const ref10 = \`myButton\`;
p8.hook = {
create: (_, n) => {
context.__owl__.refs[ref10] = n.elm;
},
destroy: () => {
delete context.__owl__.refs[ref10];
},
};
c8.push({text: \`do something\`});
}"
`;
exports[`t-slot directive slots are rendered with proper context 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils;
let h = this.h;
let c1 = extra.parentNode;
let c8 = [], p8 = {key:8,on:{}};
let vn8 = h('button', p8, c8);
c1.push(vn8);
extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p8.on['click'] = extra.handlers['click__9__'];
c8.push({text: \`do something\`});
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 2 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"Link\\"
let scope = Object.create(context);
let h = this.h;
let _11 = scope['props'].to;
let c12 = [], p12 = {key:12,attrs:{href: _11}};
let vn12 = h('a', p12, c12);
const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot13) {
slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
}
return vn12;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"App\\"
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 = [], p2 = {key:2};
let vn2 = h('u', p2, c2);
c1.push(vn2);
let _3 = scope['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
let _4 = _5 = _3;
if (!(_3 instanceof Array)) {
_4 = Object.keys(_3);
_5 = Object.values(_3);
}
let _length4 = _4.length;
let _origScope6 = scope;
scope = Object.create(scope);
for (let i1 = 0; i1 < _length4; i1++) {
scope.user_first = i1 === 0
scope.user_last = i1 === _length4 - 1
scope.user_index = i1
scope.user = _4[i1]
scope.user_value = _5[i1]
let key1 = scope['user'].id;
let c7 = [], p7 = {key:\`\${key1}_7\`};
let vn7 = h('li', p7, c7);
c2.push(vn7);
// Component 'Link'
let k9 = \`__9__\${key1}__\`;
let w8 = k9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k9]] : false;
let props8 = {to:'/user/'+scope['user'].id};
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) {
w8.destroy();
w8 = false;
}
if (w8) {
w8.__updateProps(props8, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w8.__owl__.pvnode;
c7.push(pvnode);
} else {
let componentKey8 = \`Link\`;
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| scope['Link'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
w8 = new W8(parent, props8);
parent.__owl__.cmap[k9] = w8.__owl__.id;
w8.__owl__.slotId = 1;
let fiber = w8.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
c7.push(pvnode);
w8.__owl__.pvnode = pvnode;
}
w8.__owl__.parentLastFiberId = extra.fiber.id;
}
scope = _origScope6;
return vn1;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 2 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let scope = Object.create(context);
let h = this.h;
let c7 = extra.parentNode;
c7.push({text: \`User \`});
let _10 = scope['user'].name;
if (_10 != null) {
c7.push({text: _10});
}
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 3 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"Link\\"
let scope = Object.create(context);
let h = this.h;
let _10 = scope['props'].to;
let c11 = [], p11 = {key:11,attrs:{href: _10}};
let vn11 = h('a', p11, c11);
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot12) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c11, parent: extra.parent || context}));
}
return vn11;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"App\\"
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 = [], p2 = {key:2};
let vn2 = h('u', p2, c2);
c1.push(vn2);
let _3 = scope['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
let _4 = _5 = _3;
if (!(_3 instanceof Array)) {
_4 = Object.keys(_3);
_5 = Object.values(_3);
}
let _length4 = _4.length;
let _origScope6 = scope;
scope = Object.create(scope);
for (let i1 = 0; i1 < _length4; i1++) {
scope.user_first = i1 === 0
scope.user_last = i1 === _length4 - 1
scope.user_index = i1
scope.user = _4[i1]
scope.user_value = _5[i1]
let key1 = scope['user'].id;
let c7 = [], p7 = {key:\`\${key1}_7\`};
let vn7 = h('li', p7, c7);
c2.push(vn7);
utils.getScope(scope, 'userdescr').userdescr = 'User '+scope['user'].name;
// Component 'Link'
let k9 = \`__9__\${key1}__\`;
let w8 = k9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k9]] : false;
let props8 = {to:'/user/'+scope['user'].id};
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) {
w8.destroy();
w8 = false;
}
if (w8) {
w8.__updateProps(props8, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w8.__owl__.pvnode;
c7.push(pvnode);
} else {
let componentKey8 = \`Link\`;
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| scope['Link'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
w8 = new W8(parent, props8);
parent.__owl__.cmap[k9] = w8.__owl__.id;
w8.__owl__.slotId = 1;
let fiber = w8.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
c7.push(pvnode);
w8.__owl__.pvnode = pvnode;
}
w8.__owl__.parentLastFiberId = extra.fiber.id;
}
scope = _origScope6;
return vn1;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 3 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let scope = Object.create(context);
let h = this.h;
let c7 = extra.parentNode;
if (scope.userdescr != null) {
c7.push({text: scope.userdescr});
}
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 4 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"App\\"
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);
scope.userdescr = 'User '+scope['state'].user.name;
// Component 'Link'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {to:'/user/'+scope['state'].user.id};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Link\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Link'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let scope = Object.create(context);
let h = this.h;
let c1 = extra.parentNode;
if (scope.userdescr != null) {
c1.push({text: scope.userdescr});
}
}"
`;
exports[`t-slot directive template can just return a slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let result;
let h = this.h;
const slot6 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot6) {
let children7= []
result = {}
slot6.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: children7, parent: extra.parent || context}));
utils.defineProxy(result, children7[0]);
}
return result;
}"
`;
File diff suppressed because it is too large Load Diff
+876
View File
@@ -0,0 +1,876 @@
import { Component, Env } from "../../src/component/component";
import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags";
import { useState, useRef } from "../../src/hooks";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
function children(w: Component<any, any>): Component<any, any>[] {
const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]);
}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("t-slot directive", () => {
test("can define and call slots", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog>
<t t-set="header"><span>header</span></t>
<t t-set="footer"><span>footer</span></t>
</Dialog>
</div>
<div t-name="Dialog">
<div><t t-slot="header"/></div>
<div><t t-slot="footer"/></div>
</div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div><div><span>header</span></div><div><span>footer</span></div></div></div>"
);
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Dialog.fn.toString()).toMatchSnapshot();
expect(QWeb.slots["1_header"].toString()).toMatchSnapshot();
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("named slots can define a default content", async () => {
class Dialog extends Component<any, any> {
static template = xml`
<span>
<t t-slot="header">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Dialog/></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>default content</span></div>");
expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot();
});
test("dafault slots can define a default content", async () => {
class Dialog extends Component<any, any> {
static template = xml`
<span>
<t t-slot="default">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Dialog/></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>default content</span></div>");
expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot();
});
test("default content is not rendered if slot is provided", async () => {
class Dialog extends Component<any, any> {
static template = xml`
<span>
<t t-slot="default">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Dialog>hey</Dialog></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>hey</span></div>");
});
test("default content is not rendered if named slot is provided", async () => {
class Dialog extends Component<any, any> {
static template = xml`
<span>
<t t-slot="header">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Dialog><t t-set="header">hey</t></Dialog></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>hey</span></div>");
});
test("slots are rendered with proper context", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<span class="counter"><t t-esc="state.val"/></span>
<Dialog>
<t t-set="footer"><button t-on-click="doSomething">do something</button></t>
</Dialog>
</div>
<span t-name="Dialog"><t t-slot="footer"/></span>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
state = useState({ val: 0 });
doSomething() {
this.state.val++;
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><span class="counter">0</span><span><button>do something</button></span></div>'
);
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><span class="counter">1</span><span><button>do something</button></span></div>'
);
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("slots are rendered with proper context, part 2", async () => {
env.qweb.addTemplates(`
<templates>
<a t-name="Link" t-att-href="props.to">
<t t-slot="default"/>
</a>
<div t-name="App">
<u><li t-foreach="state.users" t-as="user" t-key="user.id">
<Link to="'/user/' + user.id">User <t t-esc="user.name"/></Link>
</li></u>
</div>
</templates>
`);
class Link extends Component<any, any> {}
class App extends Component<any, any> {
state = useState({
users: [
{ id: 1, name: "Aaron" },
{ id: 2, name: "David" }
]
});
static components = { Link };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User David</a></li></u></div>'
);
expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.users[1].name = "Mathieu";
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User Mathieu</a></li></u></div>'
);
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("slots are rendered with proper context, part 3", async () => {
env.qweb.addTemplates(`
<templates>
<a t-name="Link" t-att-href="props.to">
<t t-slot="default"/>
</a>
<div t-name="App">
<u><li t-foreach="state.users" t-as="user" t-key="user.id" >
<t t-set="userdescr" t-value="'User ' + user.name"/>
<Link to="'/user/' + user.id"><t t-esc="userdescr"/></Link>
</li></u>
</div>
</templates>
`);
class Link extends Component<any, any> {}
class App extends Component<any, any> {
state = useState({
users: [
{ id: 1, name: "Aaron" },
{ id: 2, name: "David" }
]
});
static components = { Link };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User David</a></li></u></div>'
);
expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.users[1].name = "Mathieu";
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User Mathieu</a></li></u></div>'
);
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("slots are rendered with proper context, part 4", async () => {
env.qweb.addTemplates(`
<templates>
<a t-name="Link" t-att-href="props.to">
<t t-slot="default"/>
</a>
<div t-name="App">
<t t-set="userdescr" t-value="'User ' + state.user.name"/>
<Link to="'/user/' + state.user.id"><t t-esc="userdescr"/></Link>
</div>
</templates>
`);
class Link extends Component<any, any> {}
class App extends Component<any, any> {
state = useState({ user: { id: 1, name: "Aaron" } });
static components = { Link };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe('<div><a href="/user/1">User Aaron</a></div>');
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.user.name = "David";
await nextTick();
expect(fixture.innerHTML).toBe('<div><a href="/user/1">User David</a></div>');
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("refs are properly bound in slots", async () => {
class Dialog extends Component<any, any> {
static template = xml`<span><t t-slot="footer"/></span>`;
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<span class="counter"><t t-esc="state.val"/></span>
<Dialog>
<t t-set="footer"><button t-ref="myButton" t-on-click="doSomething">do something</button></t>
</Dialog>
</div>
`;
static components = { Dialog };
state = useState({ val: 0 });
button = useRef("myButton");
doSomething() {
this.state.val++;
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><span class="counter">0</span><span><button>do something</button></span></div>'
);
parent.button.el!.click();
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><span class="counter">1</span><span><button>do something</button></span></div>'
);
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("content is the default slot", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog>
<span>sts rocks</span>
</Dialog>
</div>
<div t-name="Dialog"><t t-slot="default"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>sts rocks</span></div></div>");
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("default slot work with text nodes", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog>sts rocks</Dialog>
</div>
<div t-name="Dialog"><t t-slot="default"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>sts rocks</div></div>");
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("multiple roots are allowed in a named slot", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog>
<t t-set="content">
<span>sts</span>
<span>rocks</span>
</t>
</Dialog>
</div>
<div t-name="Dialog"><t t-slot="content"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>sts</span><span>rocks</span></div></div>");
expect(QWeb.slots["1_content"].toString()).toMatchSnapshot();
});
test("multiple roots are allowed in a default slot", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog>
<span>sts</span>
<span>rocks</span>
</Dialog>
</div>
<div t-name="Dialog"><t t-slot="default"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>sts</span><span>rocks</span></div></div>");
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("missing slots are ignored", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog/>
</div>
<span t-name="Dialog">
<t t-slot="default"/>
<span>some content</span>
<t t-slot="footer"/>
</span>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span><span>some content</span></span></div>");
});
test("t-debug on a t-set (defining a slot)", async () => {
const consoleLog = console.log;
console.log = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog><t t-set="content" t-debug="">abc</t></Dialog>
</div>
<span t-name="Dialog">
<t t-slot="content"/>
</span>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(console.log).toHaveBeenCalledTimes(0);
console.log = consoleLog;
});
test("slot preserves properly parented relationship", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Child>
<GrandChild/>
</Child>
</div>
<div t-name="Child"><t t-slot="default"/></div>
<div t-name="GrandChild">Grand Child</div>
</templates>
`);
class Child extends Component<any, any> {}
class GrandChild extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Child, GrandChild };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><div>Grand Child</div></div></div>");
const parentChildren = children(parent);
expect(parentChildren.length).toBe(1);
expect(parentChildren[0]).toBeInstanceOf(Child);
const childrenChildren = children(parentChildren[0]);
expect(childrenChildren.length).toBe(1);
expect(childrenChildren[0]).toBeInstanceOf(GrandChild);
});
test("nested slots: evaluation context and parented relationship", async () => {
let slot;
class Slot extends Component<any, any> {
static template = xml`<span t-esc="props.val"/>`;
constructor(parent, props) {
super(parent, props);
slot = this;
}
}
class GrandChild extends Component<any, any> {
static template = xml`<div><t t-slot="default"/></div>`;
}
class Child extends Component<any, any> {
static components = { GrandChild };
static template = xml`
<GrandChild>
<t t-slot="default"/>
</GrandChild>`;
}
class Parent extends Component<any, any> {
static components = { Child, Slot };
static template = xml`<Child><Slot val="state.val"/></Child>`;
state = useState({ val: 3 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
expect(slot.__owl__.parent).toBeInstanceOf(GrandChild);
});
test("slot are properly rendered if inner props are changed", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
SC:<t t-esc="props.val"/>
</div>
<div t-name="GenericComponent">
<t t-slot="default" />
</div>
<div t-name="App">
<button t-on-click="inc">Inc[<t t-esc="state.val"/>]</button>
<GenericComponent>
<SomeComponent val="state.val"/>
</GenericComponent>
</div>
</templates>
`);
class SomeComponent extends Component<any, any> {}
class GenericComponent extends Component<any, any> {}
class App extends Component<any, any> {
static components = { GenericComponent, SomeComponent };
state = useState({ val: 4 });
inc() {
this.state.val++;
}
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>Inc[4]</button><div><div> SC:4</div></div></div>");
(<any>fixture.querySelector("button")).click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>Inc[5]</button><div><div> SC:5</div></div></div>");
});
test("slots and wrapper components", async () => {
class Link extends Component<any, any> {
static template = xml`
<a href="abc">
<t t-slot="default"/>
</a>`;
}
class A extends Component<any, any> {
static template = xml`<Link>hey</Link>`;
static components = { Link: Link };
}
const a = new A();
await a.mount(fixture);
expect(fixture.innerHTML).toBe(`<a href="abc">hey</a>`);
});
test("template can just return a slot", async () => {
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.value"/></span>`;
}
class SlotComponent extends Component<any, any> {
static template = xml`<t t-slot="default"/>`;
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<SlotComponent><Child value="state.value"/></SlotComponent>
</div>`;
static components = { SlotComponent, Child };
state = useState({ value: 3 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
expect(QWeb.TEMPLATES[SlotComponent.template].fn.toString()).toMatchSnapshot();
parent.state.value = 5;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>5</span></div>");
});
test("multiple slots containing components", async () => {
class C extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class B extends Component<any, any> {
static template = xml`<div><t t-slot="s1"/><t t-slot="s2"/></div>`;
}
class A extends Component<any, any> {
static template = xml`
<B>
<t t-set="s1"><C val="1"/></t>
<t t-set="s2"><C val="2"/></t>
</B>`;
static components = { B, C };
}
const a = new A();
await a.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><span>1</span><span>2</span></div>`);
});
test("slots in t-foreach and re-rendering", async () => {
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="state.val"/><t t-slot="default"/></span>`;
state = useState({ val: "A" });
mounted() {
this.state.val = "B";
}
}
class Parent extends Component<any, any> {
static components = { Child };
static template = xml`
<div>
<t t-foreach="Array(2)" t-as="n" t-key="n_index">
<Child><t t-esc="n_index"/></Child>
</t>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>A0</span><span>A1</span></div>");
await nextTick(); // wait for the changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("<div><span>B0</span><span>B1</span></div>");
});
test("slots in t-foreach with t-set and re-rendering", async () => {
class Child extends Component<any, any> {
static template = xml`
<span>
<t t-esc="state.val"/>
<t t-slot="default"/>
</span>`;
state = useState({ val: "A" });
mounted() {
this.state.val = "B";
}
}
class ParentWidget extends Component<any, any> {
static components = { Child };
static template = xml`
<div>
<t t-foreach="Array(2)" t-as="n" t-key="n_index">
<t t-set="dummy" t-value="n_index"/>
<Child><t t-esc="dummy"/></Child>
</t>
</div>`;
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>A0</span><span>A1</span></div>");
await nextTick(); // wait for changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("<div><span>B0</span><span>B1</span></div>");
});
test("nested slots in same template", async () => {
let child, child2, child3;
class Child extends Component<any, any> {
static template = xml`
<span id="c1">
<div>
<t t-slot="default"/>
</div>
</span>`;
constructor(parent, props) {
super(parent, props);
child = this;
}
}
class Child2 extends Component<any, any> {
static template = xml`
<span id="c2">
<t t-slot="default"/>
</span>`;
constructor(parent, props) {
super(parent, props);
child2 = this;
}
}
class Child3 extends Component<any, any> {
static template = xml`
<span>Child 3</span>`;
constructor(parent, props) {
super(parent, props);
child3 = this;
}
}
class Parent extends Component<any, any> {
static components = { Child, Child2, Child3 };
static template = xml`
<span id="parent">
<Child>
<Child2>
<Child3/>
</Child2>
</Child>
</span>`;
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
'<span id="parent"><span id="c1"><div><span id="c2"><span>Child 3</span></span></div></span></span>'
);
expect(child3.__owl__.parent).toStrictEqual(child2);
expect(child2.__owl__.parent).toStrictEqual(child);
expect(child.__owl__.parent).toStrictEqual(widget);
});
test("t-slot nested within another slot", async () => {
let portal, modal, child3;
class Child3 extends Component<any, any> {
static template = xml`
<span>Child 3</span>`;
constructor(parent, props) {
super(parent, props);
child3 = this;
}
}
class Modal extends Component<any, any> {
static template = xml`
<span id="modal">
<t t-slot="default"/>
</span>`;
constructor(parent, props) {
super(parent, props);
modal = this;
}
}
class Portal extends Component<any, any> {
static template = xml`
<span id="portal">
<t t-slot="default"/>
</span>`;
constructor(parent, props) {
super(parent, props);
portal = this;
}
}
class Dialog extends Component<any, any> {
static components = { Modal, Portal };
static template = xml`
<span id="c2">
<Modal>
<Portal>
<t t-slot="default"/>
</Portal>
</Modal>
</span>`;
}
class Parent extends Component<any, any> {
static components = { Child3, Dialog };
static template = xml`
<span id="c1">
<Dialog>
<Child3/>
</Dialog>
</span>`;
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
'<span id="c1"><span id="c2"><span id="modal"><span id="portal"><span>Child 3</span></span></span></span></span>'
);
expect(child3.__owl__.parent).toStrictEqual(portal);
expect(portal.__owl__.parent).toStrictEqual(modal);
});
test("t-slot supports many instances", async () => {
let child3;
class Child3 extends Component<any, any> {
static template = xml`
<span>Child 3</span>`;
constructor(parent, props) {
super(parent, props);
child3 = this;
}
}
class Dialog extends Component<any, any> {
static template = xml`
<span id="c2">
<t t-slot="default"/>
</span>`;
}
class Parent extends Component<any, any> {
static components = { Child3, Dialog };
static template = xml`
<span id="c1">
<Dialog>
<Child3 val="state.lol"/>
</Dialog>
</span>`;
state = { lol: "k" };
}
const widget = new Parent();
await widget.mount(fixture);
expect(child3.props.val).toBe("k");
const widget_1 = new Parent();
widget_1.state.lol = "m";
await widget_1.mount(fixture);
expect(child3.props.val).toBe("m");
});
test("slots in slots, with vars", async () => {
class B extends Component<any, any> {
static template = xml`<span><t t-slot="default"/></span>`;
}
class A extends Component<any, any> {
static template = xml`
<div>
<B>
<t t-slot="default"/>
</B>
</div>`;
static components = { B };
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<t t-set="test" t-value="state.name"/>
<A>
<p>hey<t t-esc="test"/></p>
</A>
</div>`;
static components = { A };
state = { name: "aaron" };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span><p>heyaaron</p></span></div></div>");
});
});
+170
View File
@@ -0,0 +1,170 @@
import { Component, Env } from "../../src/component/component";
import { processSheet } from "../../src/component/styles";
import { xml, css } from "../../src/tags";
import { makeTestFixture, makeTestEnv } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: an Env, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
document.head.innerHTML = "";
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("styles and component", () => {
test("can define an inline stylesheet", async () => {
class App extends Component<any, any> {
static template = xml`<div class="app">text</div>`;
static style = css`
.app {
color: red;
}
`;
}
expect(document.head.innerHTML).toBe("");
const app = new App();
expect(document.head.innerHTML).toBe(`<style component=\"App\">.app {
color: red;
}</style>`);
await app.mount(fixture);
const style = getComputedStyle(app.el!);
expect(style.color).toBe("red");
expect(fixture.innerHTML).toBe('<div class="app">text</div>');
});
test("inherited components properly apply css", async () => {
class App extends Component<any, any> {
static template = xml`<div class="app">text</div>`;
static style = css`
.app {
color: red;
}
`;
}
class SubApp extends App {
static style = css`
.app {
font-weight: bold;
}
`;
}
expect(document.head.innerHTML).toBe("");
const app = new SubApp();
expect(document.head.innerHTML).toBe(`<style component=\"SubApp\">.app {
font-weight: bold;
}</style><style component=\"App\">.app {
color: red;
}</style>`);
await app.mount(fixture);
const style = getComputedStyle(app.el!);
expect(style.color).toBe("red");
expect(style.fontWeight).toBe("bold");
expect(fixture.innerHTML).toBe('<div class="app">text</div>');
});
test("get a meaningful error message if css helper is missing", async () => {
class App extends Component<any, any> {
static template = xml`<div class="app">text</div>`;
static style = `.app {color: red;}`;
}
let error;
try {
new App();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(
"Invalid css stylesheet for component 'App'. Did you forget to use the 'css' tag helper?"
);
});
test("inline stylesheets are processed", async () => {
class App extends Component<any, any> {
static template = xml`<div class="app">text</div>`;
static style = css`
.app {
color: red;
.some-class {
font-weight: bold;
width: 40px;
}
display: block;
}
`;
}
new App();
expect(document.head.querySelector("style")!.innerHTML).toBe(`.app {
color: red;
}
.app .some-class {
font-weight: bold;
width: 40px;
}
.app {
display: block;
}`);
});
test("properly handle rules with commas", async () => {
const sheet = processSheet(`.parent-a, .parent-b {
.child-a, .child-b {
color: red;
}
}`);
expect(sheet)
.toBe(`.parent-a .child-a, .parent-a .child-b, .parent-b .child-a, .parent-b .child-b {
color: red;
}`);
});
test("handle & selector", async () => {
let sheet = processSheet(`.btn {
&.danger {
color: red;
}
}`);
expect(sheet).toBe(`.btn.danger {
color: red;
}`);
sheet = processSheet(`.some-class {
&.btn {
.other-class ~ & {
color: red;
}
}
}`);
expect(sheet).toBe(`.other-class ~ .some-class.btn {
color: red;
}`);
});
});
+10 -6
View File
@@ -1,4 +1,4 @@
import { Env } from "../src/component/component";
import { Env, Component } from "../src/component/component";
import { scheduler } from "../src/component/scheduler";
import { EvalContext, QWeb } from "../src/qweb/qweb";
import { CompilationContext } from "../src/qweb/compilation_context";
@@ -25,6 +25,7 @@ beforeEach(() => {
slots = Object.assign({}, QWeb.slots);
nextId = QWeb.nextId;
TEMPLATES = Object.assign({}, QWeb.TEMPLATES);
Component.scheduler.tasks = [];
});
afterEach(() => {
@@ -32,6 +33,7 @@ afterEach(() => {
QWeb.slots = slots;
QWeb.nextId = nextId;
QWeb.TEMPLATES = TEMPLATES;
Component.scheduler.tasks = [];
});
// helpers
@@ -87,6 +89,11 @@ export function renderToDOM(
context: EvalContext = {},
extra?: any
): HTMLElement | Text {
if (!context.__owl__) {
// we add `__owl__` to better simulate a component as context. This is
// particularly important for event handlers added with the `t-on` directive.
context.__owl__ = { isMounted: true };
}
const vnode = qweb.render(template, context, extra);
// we snapshot here the compiled code. This is useful to prevent unwanted code
@@ -116,11 +123,8 @@ export function renderToString(
context: EvalContext = {},
extra?: any
): string {
const node = renderToDOM(qweb, t, context, extra);
const result = node instanceof Text ? node.textContent! : node.outerHTML;
if (result !== qweb.renderToString(t, context, extra)) {
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
}
const result = qweb.renderToString(t, context, extra);
expect(qweb.templates[t].fn.toString()).toMatchSnapshot();
return result;
}
+37 -1
View File
@@ -9,7 +9,8 @@ import {
onWillPatch,
onWillStart,
onWillUpdateProps,
useSubEnv
useSubEnv,
useExternalListener
} from "../src/hooks";
import { xml } from "../src/tags";
@@ -592,4 +593,39 @@ describe("hooks", () => {
expect(fixture.innerHTML).toBe("<div><span>2</span></div>");
expect(steps).toEqual(["onWillStart", "onWillUpdateProps"]);
});
test("useExternalListener", async () => {
let n = 0;
class MyComponent extends Component<any, any> {
static template = xml`<span><t t-esc="props.value"/></span>`;
constructor(parent, props) {
super(parent, props);
useExternalListener(window as any, "click", this.increment);
}
increment() {
n++;
}
}
class App extends Component<any, any> {
static template = xml`<div><MyComponent t-if="state.flag"/></div>`;
static components = { MyComponent };
state = useState({ flag: false });
}
const app = new App();
await app.mount(fixture);
expect(n).toBe(0);
window.dispatchEvent(new Event("click"));
expect(n).toBe(0);
app.state.flag = true;
await nextTick();
window.dispatchEvent(new Event("click"));
expect(n).toBe(1);
app.state.flag = false;
await nextTick();
window.dispatchEvent(new Event("click"));
expect(n).toBe(1);
});
});
+890
View File
@@ -0,0 +1,890 @@
import { Portal } from "../../src/misc/portal";
import { xml } from "../../src/tags";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
import { Component } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { QWeb } from "../../src/qweb";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - outside: a div with id #outside appended into fixture, meant to be used as
// target by Portal component
// - a test env, necessary to create components, that is set on Component
let fixture: HTMLElement;
let outside: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
outside = document.createElement("div");
outside.setAttribute("id", "outside");
fixture.appendChild(outside);
Component.env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
describe("Portal: Props validation", () => {
test("target is mandatory", async () => {
const dev = QWeb.dev;
QWeb.dev = true;
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal>
<div>2</div>
</Portal>
</div>`;
}
let error;
try {
const parent = new Parent();
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'target' (component 'Portal')`);
QWeb.dev = dev;
});
test("target is not list", async () => {
const dev = QWeb.dev;
QWeb.dev = true;
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="['body']">
<div>2</div>
</Portal>
</div>`;
}
let error;
try {
const parent = new Parent();
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Invalid Prop 'target' in component 'Portal'`);
QWeb.dev = dev;
});
});
describe("Portal: Basic use and DOM placement", () => {
test("basic use of portal", async () => {
const dev = QWeb.dev;
QWeb.dev = true;
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<span>1</span>
<Portal target="'#outside'">
<div>2</div>
</Portal>
</div>`;
}
let error;
let parent;
try {
parent = new Parent();
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<div>2</div>");
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
QWeb.dev = dev;
});
test("conditional use of Portal", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<span>1</span>
<Portal target="'#outside'" t-if="state.hasPortal">
<div>2</div>
</Portal>
</div>`;
state = useState({ hasPortal: false });
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("");
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
parent.state.hasPortal = true;
await nextTick();
expect(outside.innerHTML).toBe("<div>2</div>");
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
parent.state.hasPortal = false;
await nextTick();
expect(outside.innerHTML).toBe("");
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
parent.state.hasPortal = true;
await nextTick();
expect(outside.innerHTML).toBe("<div>2</div>");
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
});
test("conditional use of Portal (with sub Component)", async () => {
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="props.val"/></div>`;
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<span>1</span>
<Portal t-if="state.hasPortal" target="'#outside'">
<Child val="state.val"/>
</Portal>
</div>`;
state = useState({ hasPortal: false, val: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("");
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
parent.state.hasPortal = true;
await nextTick();
expect(outside.innerHTML).toBe("<div>1</div>");
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
parent.state.hasPortal = false;
await nextTick();
expect(outside.innerHTML).toBe("");
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
parent.state.val = 2;
await nextTick();
expect(outside.innerHTML).toBe("");
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
parent.state.hasPortal = true;
await nextTick();
expect(outside.innerHTML).toBe("<div>2</div>");
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
});
test("with target in template (before portal)", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<div id="local-target"></div>
<span>1</span>
<Portal target="'#local-target'">
<p>2</p>
</Portal>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(parent.el!.innerHTML).toBe(
'<div id="local-target"><p>2</p></div><span>1</span><portal></portal>'
);
});
test("with target in template (after portal)", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<span>1</span>
<Portal target="'#local-target'">
<p>2</p>
</Portal>
<div id="local-target"></div>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(parent.el!.innerHTML).toBe(
'<span>1</span><portal></portal><div id="local-target"><p>2</p></div>'
);
});
test("portal with target not in dom", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#does-not-exist'">
<div>2</div>
</Portal>
</div>`;
}
const parent = new Parent();
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe('Could not find any match for "#does-not-exist"');
expect(console.error).toBeCalledTimes(0);
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
console.error = consoleError;
});
test("portal with child and props", async () => {
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
mounted() {
steps.push("mounted");
expect(outside.innerHTML).toBe("<span>1</span>");
}
patched() {
steps.push("patched");
expect(outside.innerHTML).toBe("<span>2</span>");
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'">
<Child val="state.val"/>
</Portal>
</div>`;
state = useState({ val: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<span>1</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
parent.state.val = 2;
await nextTick();
expect(outside.innerHTML).toBe("<span>2</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
expect(steps).toEqual(["mounted", "patched"]);
});
test("portal with only text as content", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<t t-esc="'only text'"/>
</Portal>
</div>`;
}
const parent = new Parent();
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Portal must have exactly one non-text child (has 0)");
expect(console.error).toBeCalledTimes(0);
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
console.error = consoleError;
});
test("portal with no content", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<t t-if="false" t-esc="'ABC'"/>
</Portal>
</div>`;
}
const parent = new Parent();
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Portal must have exactly one non-text child (has 0)");
expect(console.error).toBeCalledTimes(0);
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
console.error = consoleError;
});
test("portal with many children", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<div>1</div>
<p>2</p>
</Portal>
</div>`;
}
const parent = new Parent();
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Portal must have exactly one non-text child (has 2)");
expect(console.error).toBeCalledTimes(0);
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
console.error = consoleError;
});
test("portal with dynamic body", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<span t-if="state.val" t-esc="state.val"/>
<div t-else=""/>
</Portal>
</div>`;
state = useState({ val: "ab" });
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe(`<span>ab</span>`);
parent.state.val = "";
await nextTick();
expect(outside.innerHTML).toBe(`<div></div>`);
});
test("portal could have dynamically no content", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<span t-if="state.val" t-esc="state.val"/>
</Portal>
</div>`;
state = { val: "ab" };
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe(`<span>ab</span>`);
let error;
try {
parent.state.val = "";
await parent.render();
} catch (e) {
error = e;
}
expect(outside.innerHTML).toBe(``);
expect(error).toBeDefined();
expect(error.message).toBe("Portal must have exactly one non-text child (has 0)");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("lifecycle hooks of portal sub component are properly called", async () => {
const steps: any[] = [];
class Child extends Component<any, any> {
static template = xml`<span t-esc="props.val"/>`;
mounted() {
steps.push("child:mounted");
}
willPatch() {
steps.push("child:willPatch");
}
patched() {
steps.push("child:patched");
}
willUnmount() {
steps.push("child:willUnmount");
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal t-if="state.hasChild" target="'#outside'">
<Child val="state.val"/>
</Portal>
</div>`;
state = useState({ hasChild: false, val: 1 });
mounted() {
steps.push("parent:mounted");
}
willPatch() {
steps.push("parent:willPatch");
}
patched() {
steps.push("parent:patched");
}
willUnmount() {
steps.push("parent:willUnmount");
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(steps).toEqual(["parent:mounted"]);
parent.state.hasChild = true;
await nextTick();
expect(steps).toEqual([
"parent:mounted",
"parent:willPatch",
"child:mounted",
"parent:patched"
]);
parent.state.val = 2;
await nextTick();
expect(steps).toEqual([
"parent:mounted",
"parent:willPatch",
"child:mounted",
"parent:patched",
"parent:willPatch",
"child:willPatch",
"child:patched",
"parent:patched"
]);
parent.state.hasChild = false;
await nextTick();
expect(steps).toEqual([
"parent:mounted",
"parent:willPatch",
"child:mounted",
"parent:patched",
"parent:willPatch",
"child:willPatch",
"child:patched",
"parent:patched",
"parent:willPatch",
"child:willUnmount",
"parent:patched"
]);
});
test("portal destroys on crash", async () => {
class Child extends Component<any, any> {
static template = xml`<span t-esc="props.error and this.will.crash" />`;
state = {};
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'" >
<Child error="state.error"/>
</Portal>
</div>`;
state = { error: false };
}
const parent = new Parent();
await parent.mount(fixture);
parent.state.error = true;
let error;
try {
await parent.render();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
});
test("portal manual unmount", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<span>gloria</span>
</Portal>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<span>gloria</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
parent.unmount();
expect(outside.innerHTML).toBe("");
expect(parent.el!.innerHTML).toBe("<portal><span>gloria</span></portal>");
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<span>gloria</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
});
test("portal manual unmount with subcomponent", async () => {
expect.assertions(9);
class Child extends Component<any, any> {
static template = xml`<span>gloria</span>`;
mounted() {
expect(outside.contains(this.el)).toBeTruthy();
}
willUnmount() {
expect(outside.contains(this.el)).toBeTruthy();
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'">
<Child />
</Portal>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<span>gloria</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
parent.unmount();
expect(outside.innerHTML).toBe("");
expect(parent.el!.innerHTML).toBe("<portal><span>gloria</span></portal>");
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<span>gloria</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
});
});
describe("Portal: Events handling", () => {
test("events triggered on movable pure node are handled", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<span id="trigger-me" t-on-custom="_onCustom" t-esc="state.val"/>
</Portal>
</div>`;
state = useState({ val: "ab" });
_onCustom() {
this.state.val = "triggered";
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe(`<span id="trigger-me">ab</span>`);
outside.querySelector("#trigger-me")!.dispatchEvent(new Event("custom"));
await nextTick();
expect(outside.innerHTML).toBe(`<span id="trigger-me">triggered</span>`);
});
test("events triggered on movable owl components are redirected", async () => {
let childInst: Component<any, any> | null = null;
class Child extends Component<any, any> {
static template = xml`
<span t-on-custom="_onCustom" t-esc="props.val"/>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
_onCustom() {
this.trigger("custom-portal");
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div t-on-custom-portal="_onCustomPortal">
<Portal target="'#outside'">
<Child val="state.val"/>
</Portal>
</div>`;
state = useState({ val: "ab" });
_onCustomPortal() {
this.state.val = "triggered";
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe(`<span>ab</span>`);
childInst!.trigger("custom");
await nextTick();
expect(outside.innerHTML).toBe(`<span>triggered</span>`);
});
test("events triggered on contained movable owl components are redirected", async () => {
const steps: string[] = [];
let childInst: Component<any, any> | null = null;
class Child extends Component<any, any> {
static template = xml`
<span t-on-custom="_onCustom"/>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
_onCustom() {
this.trigger("custom-portal");
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div t-on-custom="_handled" t-on-custom-portal="_handled">
<Portal target="'#outside'">
<div>
<Child/>
</div>
</Portal>
</div>`;
_handled(ev) {
steps.push(ev.type);
}
}
const parent = new Parent();
await parent.mount(fixture);
childInst!.trigger("custom");
await nextTick();
// This is expected because trigger is synchronous
expect(steps).toMatchObject(["custom-portal", "custom"]);
});
test("Dom events are not mapped", async () => {
let childInst: Component<any, any> | null = null;
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`
<button>child</button>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div t-on-click="_handled">
<Portal target="'#outside'">
<Child />
</Portal>
</div>`;
_handled(ev) {
steps.push(ev.type as string);
}
}
const bodyListener = ev => {
steps.push(`body: ${ev.type}`);
};
document.body.addEventListener("click", bodyListener);
const parent = new Parent();
await parent.mount(fixture);
childInst!.el!.click();
expect(steps).toEqual(["body: click"]);
document.body.removeEventListener("click", bodyListener);
});
test("Nested portals event propagation", async () => {
const outside2 = document.createElement("div");
outside2.setAttribute("id", "outside2");
fixture.appendChild(outside2);
const steps: Array<string> = [];
let childInst: Component<any, any> | null = null;
class Child2 extends Component<any, any> {
static template = xml`<div>child2</div>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
}
class Child extends Component<any, any> {
static components = { Portal, Child2 };
static template = xml`
<Portal target="'#outside2'">
<Child2 />
</Portal>`;
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div t-on-custom='_handled'>
<Portal target="'#outside'">
<Child/>
</Portal>
</div>`;
_handled(ev) {
steps.push(`${ev.type} from ${ev.originalComponent.constructor.name}`);
}
}
const parent = new Parent();
await parent.mount(fixture);
childInst!.trigger("custom");
expect(steps).toEqual(["custom from Child2"]);
});
test("portal's parent's env is not polluted", async () => {
class Child extends Component<any, any> {
static template = xml`
<button>child</button>`;
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'">
<Child />
</Portal>
</div>`;
}
const parent = new Parent();
const parentEnv = Object.assign({}, parent.env);
await parent.mount(fixture);
expect(parentEnv).toStrictEqual(parent.env);
});
test("Portal composed with t-slot", async () => {
const steps: Array<string> = [];
let childInst: Component<any, any> | null = null;
class Child2 extends Component<any, any> {
static template = xml`<div>child2</div>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
}
class Child extends Component<any, any> {
static components = { Portal, Child2 };
static template = xml`
<Portal target="'#outside'">
<t t-slot="default"/>
</Portal>`;
}
class Parent extends Component<any, any> {
static components = { Child, Child2 };
static template = xml`
<div t-on-custom='_handled'>
<Child>
<Child2/>
</Child>
</div>`;
_handled(ev) {
steps.push(ev.type as string);
}
}
const parent = new Parent();
await parent.mount(fixture);
childInst!.trigger("custom");
expect(steps).toEqual(["custom"]);
});
});
describe("Portal: UI/UX", () => {
test("focus is kept across re-renders", async () => {
class Child extends Component<any, any> {
static template = xml`
<input id="target-me" t-att-placeholder="props.val"/>`;
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'">
<Child val="state.val"/>
</Portal>
</div>`;
state = useState({ val: "ab" });
}
const parent = new Parent();
await parent.mount(fixture);
const input = document.querySelector("#target-me");
expect(input!.nodeName).toBe("INPUT");
expect((input as HTMLInputElement).placeholder).toBe("ab");
(input as HTMLInputElement).focus();
expect(document.activeElement === input).toBeTruthy();
parent.state.val = "bc";
await nextTick();
const inputReRendered = document.querySelector("#target-me");
expect(inputReRendered!.nodeName).toBe("INPUT");
expect((inputReRendered as HTMLInputElement).placeholder).toBe("bc");
expect(document.activeElement === inputReRendered).toBeTruthy();
});
});
File diff suppressed because it is too large Load Diff
+294 -11
View File
@@ -46,6 +46,16 @@ describe("static templates", () => {
expect(renderToString(qweb, "test")).toBe("<div>word</div>");
});
test("div with a class attribute", () => {
qweb.addTemplate("test", `<div class="abc">word</div>`);
expect(renderToString(qweb, "test")).toBe(`<div class="abc">word</div>`);
});
test("div with a empty class attribute", () => {
qweb.addTemplate("test", `<div class="">word</div>`);
expect(renderToString(qweb, "test")).toBe(`<div>word</div>`);
});
test("div with a span child node", () => {
qweb.addTemplate("test", "<div><span>word</span></div>");
expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>");
@@ -110,9 +120,11 @@ describe("t-esc", () => {
expect(renderToString(qweb, "test", { var: "ok" })).toBe("<span>ok</span>");
});
test.skip("escaping", () => {
test("escaping", () => {
qweb.addTemplate("test", `<span><t t-esc="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<ok>" })).toBe("<span>&lt;ok&gt;</span>");
expect(renderToString(qweb, "test", { var: "<ok>abc</ok>" })).toBe(
"<span>&amp;lt;ok&amp;gt;abc&amp;lt;/ok&amp;gt;</span>"
);
});
test("escaping on a node", () => {
@@ -129,6 +141,49 @@ describe("t-esc", () => {
qweb.addTemplate("test", `<span t-esc="var">nope</span>`);
expect(renderToString(qweb, "test")).toBe("<span>nope</span>");
});
test("t-esc is escaped", () => {
qweb.addTemplate("test", `<div><t t-set="var"><p>escaped</p></t><t t-esc="var"/></div>`);
const domRendered = renderToDOM(qweb, "test");
expect(domRendered.textContent).toBe("<p>escaped</p>");
});
test("t-esc=0 is escaped", () => {
qweb.addTemplate("test", `<span><t t-esc="0"/></span>`);
qweb.addTemplate("testCaller", `<div><t t-call="test"><p>escaped</p></t></div>`);
const domRendered = renderToDOM(qweb, "testCaller") as HTMLElement;
expect(domRendered.querySelector("span")!.textContent).toBe("<p>escaped</p>");
});
test("div with falsy values", () => {
qweb.addTemplate(
"test",
`
<div>
<p t-esc="v1"/>
<p t-esc="v2"/>
<p t-esc="v3"/>
<p t-esc="v4"/>
<p t-esc="v5"/>
</div>`
);
const vals = {
v1: false,
v2: undefined,
v3: null,
v4: 0,
v5: ""
};
expect(renderToString(qweb, "test", vals)).toBe(
"<div><p>false</p><p></p><p></p><p>0</p><p></p></div>"
);
});
test("t-esc work with spread operator", () => {
qweb.addTemplate("test", `<span><t t-esc="[...state.list]"/></span>`);
const result = renderToString(qweb, "test", { state: { list: [1, 2] } });
expect(result).toBe("<span>1,2</span>");
});
});
describe("t-raw", () => {
@@ -235,6 +290,18 @@ describe("t-set", () => {
);
});
test("t-set with content and sub t-esc", () => {
qweb.addTemplate(
"test",
`<div>
<t t-set="setvar"><t t-esc="beep"/> boop</t>
<t t-esc="setvar"/>
</div>`
);
expect(renderToString(qweb, "test", { beep: "beep" })).toBe("<div>beep boop</div>");
});
test("evaluate value expression, part 2", () => {
qweb.addTemplate(
"test",
@@ -268,6 +335,58 @@ describe("t-set", () => {
expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>");
expect(renderToString(qweb, "test", { flag: false })).toBe("<div>0</div>");
});
test("t-set body is evaluated immediately", () => {
qweb.addTemplate(
"test",
`<div>
<t t-set="v1" t-value="'before'"/>
<t t-set="v2">
<span><t t-esc="v1"/></span>
</t>
<t t-set="v1" t-value="'after'"/>
<t t-raw="v2"/>
</div>`
);
expect(renderToString(qweb, "test")).toBe("<div><span>before</span></div>");
});
test("t-set with t-value (falsy) and body", () => {
qweb.addTemplate(
"test",
`<div>
<t t-set="v3" t-value="false"/>
<t t-set="v1" t-value="'before'"/>
<t t-set="v2" t-value="v3">
<span><t t-esc="v1"/></span>
</t>
<t t-set="v1" t-value="'after'"/>
<t t-set="v3" t-value="true"/>
<t t-raw="v2"/>
</div>`
);
expect(renderToString(qweb, "test")).toBe("<div><span>before</span></div>");
});
test("t-set with t-value (truthy) and body", () => {
qweb.addTemplate(
"test",
`<div>
<t t-set="v3" t-value="'Truthy'"/>
<t t-set="v1" t-value="'before'"/>
<t t-set="v2" t-value="v3">
<span><t t-esc="v1"/></span>
</t>
<t t-set="v1" t-value="'after'"/>
<t t-set="v3" t-value="false"/>
<t t-raw="v2"/>
</div>`
);
expect(renderToString(qweb, "test")).toBe("<div>Truthy</div>");
});
});
describe("t-if", () => {
@@ -355,7 +474,7 @@ describe("t-if", () => {
});
test("t-esc with t-elif", () => {
qweb.addTemplate("test", `<div><t t-if="false">abc</t><t t-else="1" t-esc="'x'"/></div>`);
qweb.addTemplate("test", `<div><t t-if="false">abc</t><t t-else="" t-esc="'x'"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>x</div>");
});
@@ -432,6 +551,18 @@ describe("attributes", () => {
expect(result).toBe(`<div foo="bar"></div>`);
});
test("dynamic class attribute", () => {
qweb.addTemplate("test", `<div t-att-class="c"/>`);
const result = renderToString(qweb, "test", { c: "abc" });
expect(result).toBe(`<div class="abc"></div>`);
});
test("dynamic empty class attribute", () => {
qweb.addTemplate("test", `<div t-att-class="c"/>`);
const result = renderToString(qweb, "test", { c: "" });
expect(result).toBe(`<div></div>`);
});
test("dynamic attribute with a dash", () => {
qweb.addTemplate("test", `<div t-att-data-action-id="id"/>`);
const result = renderToString(qweb, "test", { id: 32 });
@@ -535,7 +666,7 @@ describe("attributes", () => {
expect(result).toBe(`<div foo="a 0 is 1 of 2 ]"></div>`);
});
test.skip("various escapes", () => {
test("various escapes", () => {
// not needed??
qweb.addTemplate(
"test",
@@ -551,7 +682,7 @@ describe("attributes", () => {
baz: 1,
qux: { qux: "<>" }
});
const expected = `<div foo="&lt;foo" bar="&lt;bar&gt;" baz="&lt;&quot;&lt;baz&gt;&quot;&gt;" qux="&lt;&gt;"></div>`;
const expected = '<div foo="<foo" bar="0" baz="<1>" qux="<>"></div>';
expect(result).toBe(expected);
});
@@ -576,10 +707,19 @@ describe("attributes", () => {
describe("t-call (template calling", () => {
test("basic caller", () => {
qweb.addTemplate("_basic-callee", "<span>ok</span>");
qweb.addTemplate("caller", '<div><t t-call="_basic-callee"/></div>');
const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller")).toBe(expected);
expect(qweb.subTemplates["_basic-callee"].toString()).toMatchSnapshot();
});
test("basic caller, no parent node", () => {
qweb.addTemplate("_basic-callee", "<div>ok</div>");
qweb.addTemplate("caller", '<t t-call="_basic-callee"/>');
const expected = "<div>ok</div>";
expect(renderToString(qweb, "caller")).toBe(expected);
expect(qweb.subTemplates["_basic-callee"].toString()).toMatchSnapshot();
});
test("t-call with t-if", () => {
@@ -587,6 +727,7 @@ describe("t-call (template calling", () => {
qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>');
const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller", { flag: true })).toBe(expected);
expect(qweb.subTemplates["sub"].toString()).toMatchSnapshot();
});
test("t-call not allowed on a non t node", () => {
@@ -671,6 +812,7 @@ describe("t-call (template calling", () => {
`);
const expected = "<div><div><span>hey</span> <span>yay</span></div></div>";
expect(renderToString(qweb, "main")).toBe(expected);
expect(qweb.subTemplates["SubTemplate"].toString()).toMatchSnapshot();
});
test("cascading t-call t-raw='0'", () => {
@@ -720,7 +862,7 @@ describe("t-call (template calling", () => {
`);
const expected = "<div><span>hey</span></div>";
expect(renderToString(qweb, "recursive")).toBe(expected);
const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
const recursiveFn = Object.values(qweb.subTemplates)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot();
});
@@ -748,7 +890,7 @@ describe("t-call (template calling", () => {
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe(
expected
);
const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
const recursiveFn = Object.values(qweb.subTemplates)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot();
});
@@ -775,7 +917,7 @@ describe("t-call (template calling", () => {
const expected =
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>";
expect(renderToString(qweb, "Parent", { root }, { fiber: {} })).toBe(expected);
const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
const recursiveFn = Object.values(qweb.subTemplates)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot();
});
@@ -785,6 +927,68 @@ describe("t-call (template calling", () => {
const expected = "<div><span>desk</span></div>";
expect(trim(renderToString(qweb, "abcd"))).toBe(expected);
});
test("t-call, conditional and t-set in t-call body", () => {
QWeb.registerTemplate("callee1", "<div>callee1</div>");
QWeb.registerTemplate("callee2", '<div>callee2 <t t-esc="v"/></div>');
QWeb.registerTemplate(
"caller",
`<div>
<t t-set="v1" t-value="'elif'"/>
<t t-if="v1 === 'if'" t-call="callee1" />
<t t-elif="v1 === 'elif'" t-call="callee2" >
<t t-set="v" t-value="'success'" />
</t>
</div>`
);
const rendered = renderToString(qweb, "caller");
expect(rendered).toBe(`<div><div>callee2 success</div></div>`);
});
test("t-call with t-set inside and outside", () => {
qweb.addTemplates(`
<templates>
<div t-name="main">
<t t-foreach="list" t-as="v">
<t t-set="val" t-value="v.val"/>
<t t-call="sub">
<t t-set="val3" t-value="val*3"/>
</t>
</t>
</div>
<t t-name="sub">
<span t-esc="val3"/>
</t>
</templates>
`);
const expected = "<div><span>3</span><span>6</span><span>9</span></div>";
const context = { list: [{ val: 1 }, { val: 2 }, { val: 3 }] };
expect(trim(renderToString(qweb, "main", context))).toBe(expected);
});
test("t-call with t-set inside and outside. 2", () => {
qweb.addTemplates(`
<templates>
<div t-name="main">
<t t-foreach="list" t-as="v">
<t t-set="val" t-value="v.val"/>
<t t-call="sub">
<t t-set="val3" t-value="val*3"/>
</t>
</t>
</div>
<t t-name="sub">
<span t-esc="val3"/>
<t t-esc="w"/>
</t>
<p t-name="wrapper"><t t-set="w" t-value="'fromwrapper'"/><t t-call="main"/></p>
</templates>
`);
const expected =
"<p><div><span>3</span>fromwrapper<span>6</span>fromwrapper<span>9</span>fromwrapper</div></p>";
const context = { list: [{ val: 1 }, { val: 2 }, { val: 3 }] };
expect(trim(renderToString(qweb, "wrapper", context))).toBe(expected);
});
});
describe("foreach", () => {
@@ -853,9 +1057,9 @@ describe("foreach", () => {
<t t-foreach="[1]" t-as="item"><t t-esc="item"/></t>
</div>`
);
const context = {};
const context = { __owl__: {} };
renderToString(qweb, "test", context);
expect(Object.keys(context).length).toBe(0);
expect(Object.keys(context)).toEqual(["__owl__"]);
});
test("t-foreach in t-forach", () => {
@@ -1133,6 +1337,69 @@ describe("t-on", () => {
expect(owner.state.counter).toBe(2);
});
test("t-on with inline statement, part 2", () => {
qweb.addTemplate("test", `<button t-on-click="state.flag = !state.flag">Toggle</button>`);
let owner = {
state: {
flag: true
}
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.flag).toBe(true);
(<HTMLElement>node).click();
expect(owner.state.flag).toBe(false);
(<HTMLElement>node).click();
expect(owner.state.flag).toBe(true);
});
test("t-on with inline statement, part 3", () => {
qweb.addTemplate("test", `<button t-on-click="state.n = someFunction(3)">Toggle</button>`);
let owner = {
someFunction(n) {
return n + 1;
},
state: {
n: 11
}
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.n).toBe(11);
(<HTMLElement>node).click();
expect(owner.state.n).toBe(4);
});
test("t-on with t-call", async () => {
expect.assertions(2);
qweb.addTemplate("sub", `<p t-on-click="update">lucas</p>`);
qweb.addTemplate("main", `<div><t t-call="sub"/></div>`);
let owner = {
update() {
expect(this).toBe(owner);
}
};
const node = renderToDOM(qweb, "main", owner, { handlers: [] });
(<HTMLElement>node).querySelector("p")!.click();
});
test("t-on, with arguments and t-call", async () => {
expect.assertions(3);
qweb.addTemplate("sub", `<p t-on-click="update(value)">lucas</p>`);
qweb.addTemplate("main", `<div><t t-call="sub"/></div>`);
let owner = {
update(val) {
expect(this).toBe(owner);
expect(val).toBe(444);
},
value: 444
};
const node = renderToDOM(qweb, "main", owner, { handlers: [] });
(<HTMLElement>node).querySelector("p")!.click();
});
test("t-on with prevent and/or stop modifiers", async () => {
expect.assertions(7);
qweb.addTemplate(
@@ -1505,7 +1772,7 @@ describe("debugging", () => {
console.log = jest.fn();
qweb.addTemplate(
"test",
`<div t-debug="1"><t t-if="true"><span t-debug="1">hey</span></t></div>`
`<div t-debug=""><t t-if="true"><span t-debug="">hey</span></t></div>`
);
qweb.render("test");
expect(qweb.templates.test.fn.toString()).toMatchSnapshot();
@@ -1514,6 +1781,22 @@ describe("debugging", () => {
console.log = consoleLog;
});
test("t-debug on sub template", () => {
const consoleLog = console.log;
console.log = jest.fn();
qweb.addTemplates(`
<templates>
<p t-name="sub" t-debug="">coucou</p>
<div t-name="test">
<t t-call="sub"/>
</div>
</templates>`);
qweb.render("test");
expect(console.log).toHaveBeenCalledTimes(1);
console.log = consoleLog;
});
test("t-log", () => {
const consoleLog = console.log;
console.log = jest.fn();
+62 -29
View File
@@ -50,6 +50,18 @@ describe("tokenizer", () => {
{ type: "OPERATOR", value: "typeof " },
{ type: "SYMBOL", value: "a" }
]);
expect(tokenize("a...1")).toEqual([
{ type: "SYMBOL", value: "a" },
{ type: "OPERATOR", value: "..." },
{ type: "VALUE", value: "1" }
]);
expect(tokenize("a in b")).toEqual([
{ type: "SYMBOL", value: "a" },
{ type: "OPERATOR", value: "in " },
{ type: "SYMBOL", value: "b" }
]);
});
test("strings", () => {
@@ -92,7 +104,7 @@ describe("expression evaluation", () => {
test("parenthesis", () => {
expect(compileExpr("(1)", {})).toBe("(1)");
expect(compileExpr("a*(1 +3)", {})).toBe("context['a']*(1+3)");
expect(compileExpr("a*(1 +3)", {})).toBe("scope['a']*(1+3)");
});
test("objects and sub objects", () => {
@@ -100,8 +112,8 @@ describe("expression evaluation", () => {
});
test("replacing variables", () => {
expect(compileExpr("a", {})).toBe("context['a']");
expect(compileExpr("a", { a: { id: "_3", expr: "" } })).toBe("_3");
expect(compileExpr("a", {})).toBe("scope['a']");
expect(compileExpr("a", { a: { id: "_3", expr: "scope._3" } })).toBe("scope._3");
});
test("arrays and objects", () => {
@@ -111,56 +123,77 @@ describe("expression evaluation", () => {
});
test("dot operator", () => {
expect(compileExpr("a.b", {})).toBe("context['a'].b");
expect(compileExpr("a.b.c", {})).toBe("context['a'].b.c");
expect(compileExpr("a.b", {})).toBe("scope['a'].b");
expect(compileExpr("a.b.c", {})).toBe("scope['a'].b.c");
});
test("various unary operators", () => {
expect(compileExpr("!flag", {})).toBe("!context['flag']");
expect(compileExpr("!flag", {})).toBe("!scope['flag']");
expect(compileExpr("-3", {})).toBe("-3");
expect(compileExpr("-a", {})).toBe("-context['a']");
expect(compileExpr("typeof a", {})).toBe("typeof context['a']");
expect(compileExpr("-a", {})).toBe("-scope['a']");
expect(compileExpr("typeof a", {})).toBe("typeof scope['a']");
});
test("various binary operators", () => {
expect(compileExpr("color == 'black'", {})).toBe("context['color']=='black'");
expect(compileExpr("a || b", {})).toBe("context['a']||context['b']");
expect(compileExpr("color === 'black'", {})).toBe("context['color']==='black'");
expect(compileExpr("'li_'+item", {})).toBe("'li_'+context['item']");
expect(compileExpr("state.val > 1", {})).toBe("context['state'].val>1");
expect(compileExpr("color == 'black'", {})).toBe("scope['color']=='black'");
expect(compileExpr("a || b", {})).toBe("scope['a']||scope['b']");
expect(compileExpr("color === 'black'", {})).toBe("scope['color']==='black'");
expect(compileExpr("'li_'+item", {})).toBe("'li_'+scope['item']");
expect(compileExpr("state.val > 1", {})).toBe("scope['state'].val>1");
expect(compileExpr("a in b", {})).toBe("scope['a']in scope['b']");
});
test("boolean operations", () => {
expect(compileExpr("a && b", {})).toBe("context['a']&&context['b']");
expect(compileExpr("a && b", {})).toBe("scope['a']&&scope['b']");
});
test("ternary operators", () => {
expect(compileExpr("a ? b: '2'", {})).toBe("context['a']?context['b']:'2'");
expect(compileExpr("a ? b: (c or '2') ", {})).toBe(
"context['a']?context['b']:(context['c']||'2')"
);
expect(compileExpr("a ? b: '2'", {})).toBe("scope['a']?scope['b']:'2'");
expect(compileExpr("a ? b: (c or '2') ", {})).toBe("scope['a']?scope['b']:(scope['c']||'2')");
expect(compileExpr("a ? {test:c}: [1,u]", {})).toBe(
"context['a']?{test:context['c']}:[1,context['u']]"
"scope['a']?{test:scope['c']}:[1,scope['u']]"
);
});
test("word replacement", () => {
expect(compileExpr("a or b", {})).toBe("context['a']||context['b']");
expect(compileExpr("a and b", {})).toBe("context['a']&&context['b']");
expect(compileExpr("a or b", {})).toBe("scope['a']||scope['b']");
expect(compileExpr("a and b", {})).toBe("scope['a']&&scope['b']");
});
test("function calls", () => {
expect(compileExpr("a()", {})).toBe("context['a']()");
expect(compileExpr("a(1)", {})).toBe("context['a'](1)");
expect(compileExpr("a(1,2)", {})).toBe("context['a'](1,2)");
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe("context['a'](1,2,{a:[context['a']]})");
expect(compileExpr("a()", {})).toBe("scope['a']()");
expect(compileExpr("a(1)", {})).toBe("scope['a'](1)");
expect(compileExpr("a(1,2)", {})).toBe("scope['a'](1,2)");
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe("scope['a'](1,2,{a:[scope['a']]})");
expect(compileExpr("'x'.toUpperCase()", {})).toBe("'x'.toUpperCase()");
expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe("'x'.toUpperCase({a:3})");
expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })).toBe(
"'x'.toUpperCase(_v5)"
expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "scope._v5" } })).toBe(
"'x'.toUpperCase(scope._v5)"
);
expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })).toBe(
"'x'.toUpperCase({b:_v5})"
expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "scope._v5" } })).toBe(
"'x'.toUpperCase({b:scope._v5})"
);
});
test("arrow functions", () => {
expect(compileExpr("list.map(e => e.val)", {})).toBe("scope['list'].map(e=>e.val)");
expect(compileExpr("list.map(e => a + e)", {})).toBe("scope['list'].map(e=>scope['a']+e)");
expect(compileExpr("list.map((e) => e)", {})).toBe("scope['list'].map((e)=>e)");
expect(compileExpr("list.map((elem, index) => elem + index)", {})).toBe(
"scope['list'].map((elem,index)=>elem+index)"
);
});
test("assignation", () => {
expect(compileExpr("a = b", {})).toBe("scope['a']=scope['b']");
expect(compileExpr("a += b", {})).toBe("scope['a']+=scope['b']");
expect(compileExpr("a -= b", {})).toBe("scope['a']-=scope['b']");
expect(compileExpr("a.b = !a.b", {})).toBe("scope['a'].b=!scope['a'].b");
});
test("spread operator", () => {
expect(compileExpr("[...state.list]", {})).toBe("[...scope['state'].list]");
expect(compileExpr("f(...state.list)", {})).toBe("scope['f'](...scope['state'].list)");
expect(compileExpr("f([...list])", {})).toBe("scope['f']([...scope['list']])");
});
});
+14 -13
View File
@@ -1,21 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Link component can render simple cases 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let utils = this.constructor.utils;
let owner = context;
var h = this.h;
let _6 = utils.toObj({'router-link-active':context['isActive']});
var _7 = context['href'];
let c8 = [], p8 = {key:8,attrs:{href: _7},class:_6,on:{}};
var vn8 = h('a', p8, c8);
extra.handlers['click' + 8] = extra.handlers['click' + 8] || function (e) {const fn = context['navigate'];if (fn) { fn.call(owner, e); } else { context.navigate; }};
p8.on['click'] = extra.handlers['click' + 8];
const slot9 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot9) {
slot9.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c8, vars: extra.vars, parent: extra.parent || owner}));
let scope = Object.create(context);
let h = this.h;
let _4 = utils.toObj({'router-link-active':scope['isActive']});
let _5 = scope['href'];
let c6 = [], p6 = {key:6,attrs:{href: _5},class:_4,on:{}};
let vn6 = h('a', p6, c6);
extra.handlers['click__7__'] = extra.handlers['click__7__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['navigate'](e);};
p6.on['click'] = extra.handlers['click__7__'];
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot8) {
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c6, parent: extra.parent || context}));
}
return vn8;
return vn6;
}"
`;
@@ -1,44 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RouteComponent can render simple cases 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let scope = Object.create(context);
let result;
var h = this.h;
if (context['routeComponent']) {
const nodeKey6 = context['env'].router.currentRouteName;
//COMPONENT
let k9 = \`__10__\` + nodeKey6;
let w8 = k9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k9]] : false;
let vn11 = {};
result = vn11;
let props8 = Object.assign({}, context['env'].router.currentParams);
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) {
w8.destroy();
w8 = false;
let h = this.h;
if (scope['routeComponent']) {
{
let key0 = scope['env'].router.currentRouteName;
// Component 'routeComponent'
let k5 = \`__5__\${key0}__\`;
let w4 = k5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k5]] : false;
let vn6 = {};
result = vn6;
let props4 = Object.assign({}, scope['env'].router.currentParams);
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
w4.destroy();
w4 = false;
}
if (w4) {
w4.__updateProps(props4, extra.fiber, undefined);
let pvnode = w4.__owl__.pvnode;
utils.defineProxy(vn6, pvnode);
} else {
let componentKey4 = \`routeComponent\`;
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| scope['routeComponent'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[k5] = w4.__owl__.id;
let fiber = w4.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k5, hook: {remove() {},destroy(vn) {w4.destroy();}}});
utils.defineProxy(vn6, pvnode);
w4.__owl__.pvnode = pvnode;
}
w4.__owl__.parentLastFiberId = extra.fiber.id;
}
if (w8) {
w8.__updateProps(props8, extra.fiber, undefined, undefined);
let pvnode = w8.__owl__.pvnode;
utils.defineProxy(vn11, pvnode);
} else {
let componentKey8 = \`routeComponent\`;
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| context['routeComponent'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
w8 = new W8(parent, props8);
parent.__owl__.cmap[k9] = w8.__owl__.id;
let def7 = w8.__prepare(extra.fiber, undefined, undefined);
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
const fiber = w8.__owl__.currentFiber;
def7.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
utils.defineProxy(vn11, pvnode);
w8.__owl__.pvnode = pvnode;
}
w8.__owl__.parentLastFiberId = extra.fiber.id;
}
return result;
}"
+47 -1
View File
@@ -49,10 +49,13 @@ describe("connecting a component to store", () => {
});
test("useStore can observe primitive types and call onUpdate", async () => {
const state = { isBoolean: false };
const state = { isBoolean: false, nullValue: null };
const actions = {
setTrue({ state }) {
state.isBoolean = true;
},
setNotNull({ state }) {
state.nullValue = "ok";
}
};
const store = new Store({ state, actions });
@@ -61,8 +64,10 @@ describe("connecting a component to store", () => {
static template = xml`
<div>
<span t-if="isBoolean">ok</span>
<span t-if="nullValue !== null">not null</span>
</div>`;
isBoolean: boolean;
nullValue: string;
constructor() {
super();
this.isBoolean = useStore(state => state.isBoolean, {
@@ -70,6 +75,11 @@ describe("connecting a component to store", () => {
this.isBoolean = isBoolean;
}
});
this.nullValue = useStore(state => state.nullValue, {
onUpdate: nullValue => {
this.nullValue = nullValue;
}
});
}
}
@@ -82,6 +92,42 @@ describe("connecting a component to store", () => {
store.dispatch("setTrue");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>ok</span></div>");
store.dispatch("setNotNull");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>ok</span><span>not null</span></div>");
});
test("map works on the result of useStore when the resulting array changes for a bigger one", async () => {
const state = { smallerArray: [1], biggerArray: [2, 3], useSmallArray: true };
const store = new Store({ state });
class App extends Component<any, any> {
static template = xml`<div t-esc="mapAdd"/>`;
storeProps = {
array: useStore(state => {
if (state.useSmallArray) {
return state.smallerArray;
}
return state.biggerArray;
})
};
get mapAdd() {
return this.storeProps.array.map(a => {
return a + 1;
});
}
}
(<any>env).store = store;
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>2</div>");
store.state.useSmallArray = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div>3,4</div>");
});
test("throw error if no store is found", async () => {
+57
View File
@@ -0,0 +1,57 @@
/**
* We can only make one test per file, since the debug tool modify in place
* the owl object in a way that is difficult to undo.
*/
import { debugOwl } from "../../tools/debug";
import * as owl from "../../src/index";
import { Component, Env } from "../../src/component/component";
import { xml } from "../../src/tags";
import { useState } from "../../src/hooks";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
let fixture: HTMLElement = makeTestFixture();
let env: Env = makeTestEnv();
Component.env = env;
debugOwl(owl, {});
test("can log full lifecycle", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
class Child extends Component<any, any> {
static template = xml`<div>child</div>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Child t-if="state.flag"/></div>`;
static components = { Child };
state = useState({ flag: false });
}
const parent = new Parent(null, {});
await parent.mount(fixture);
parent.state.flag = true;
await nextTick();
expect(steps).toEqual([
"[OWL_DEBUG] Parent<id=1> constructor, props={}",
"[OWL_DEBUG] Parent<id=1> mount",
"[OWL_DEBUG] Parent<id=1> willStart",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] Parent<id=1> render",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] Child<id=2> constructor, props={}",
"[OWL_DEBUG] Child<id=2> willStart",
"[OWL_DEBUG] Child<id=2> rendering template",
"[OWL_DEBUG] Parent<id=1> willPatch",
"[OWL_DEBUG] Child<id=2> mounted",
"[OWL_DEBUG] Parent<id=1> patched"
]);
console.log = log;
});
+50
View File
@@ -0,0 +1,50 @@
/**
* We can only make one test per file, since the debug tool modify in place
* the owl object in a way that is difficult to undo.
*/
import { debugOwl } from "../../tools/debug";
import * as owl from "../../src/index";
import { Component, Env } from "../../src/component/component";
import { xml } from "../../src/tags";
import { makeTestFixture, makeTestEnv } from "../helpers";
let fixture: HTMLElement = makeTestFixture();
let env: Env = makeTestEnv();
Component.env = env;
debugOwl(owl, { logScheduler: true });
test("can log scheduler start and stop", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
class Child extends Component<any, any> {
static template = xml`<div>child</div>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Child /></div>`;
static components = { Child };
}
const parent = new Parent(null, {});
await parent.mount(fixture);
expect(steps).toEqual([
"[OWL_DEBUG] Parent<id=1> constructor, props={}",
"[OWL_DEBUG] Parent<id=1> mount",
"[OWL_DEBUG] Parent<id=1> willStart",
"[OWL_DEBUG] scheduler: start running tasks queue",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] Child<id=2> constructor, props={}",
"[OWL_DEBUG] Child<id=2> willStart",
"[OWL_DEBUG] Child<id=2> rendering template",
"[OWL_DEBUG] Child<id=2> mounted",
"[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue"
]);
console.log = log;
});
+46
View File
@@ -0,0 +1,46 @@
/**
* We can only make one test per file, since the debug tool modify in place
* the owl object in a way that is difficult to undo.
*/
import { debugOwl } from "../../tools/debug";
import * as owl from "../../src/index";
import { Component, Env } from "../../src/component/component";
import { xml } from "../../src/tags";
import { makeTestFixture, makeTestEnv } from "../helpers";
let fixture: HTMLElement = makeTestFixture();
let env: Env = makeTestEnv();
Component.env = env;
debugOwl(owl, { logScheduler: true });
test("log a specific message for render method calls if component is not mounted", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
class Parent extends Component<any, any> {
static template = xml`<div><t t-esc="state.value"/></div>`;
state = owl.hooks.useState({ value: 1 });
}
const parent = new Parent(null, {});
await parent.mount(fixture);
parent.unmount();
parent.state.value = 2;
expect(steps).toEqual([
"[OWL_DEBUG] Parent<id=1> constructor, props={}",
"[OWL_DEBUG] Parent<id=1> mount",
"[OWL_DEBUG] Parent<id=1> willStart",
"[OWL_DEBUG] scheduler: start running tasks queue",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue",
"[OWL_DEBUG] Parent<id=1> willUnmount",
"[OWL_DEBUG] Parent<id=1> render (warning: component is not mounted, this render has no effect)"
]);
console.log = log;
});
+57
View File
@@ -0,0 +1,57 @@
/**
* We can only make one test per file, since the debug tool modify in place
* the owl object in a way that is difficult to undo.
*/
import { debugOwl } from "../../tools/debug";
import * as owl from "../../src/index";
import { Component, Env } from "../../src/component/component";
import { xml } from "../../src/tags";
import { makeTestFixture, makeTestEnv } from "../helpers";
let fixture: HTMLElement = makeTestFixture();
let env: Env = makeTestEnv();
Component.env = env;
debugOwl(owl, { logScheduler: true });
test("log a sub component with non stringifiable props", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.obj.val"/></span>`;
}
const circularObject: any = { val: 1 };
circularObject.circularObject = circularObject;
class Parent extends Component<any, any> {
static template = xml`<div><Child obj="obj"/></div>`;
static components = { Child };
obj = circularObject;
}
const parent = new Parent(null, {});
await parent.mount(fixture);
parent.unmount();
expect(steps).toEqual([
"[OWL_DEBUG] Parent<id=1> constructor, props={}",
"[OWL_DEBUG] Parent<id=1> mount",
"[OWL_DEBUG] Parent<id=1> willStart",
"[OWL_DEBUG] scheduler: start running tasks queue",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] Child<id=2> constructor, props=<JSON error>",
"[OWL_DEBUG] Child<id=2> willStart",
"[OWL_DEBUG] Child<id=2> rendering template",
"[OWL_DEBUG] Child<id=2> mounted",
"[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue",
"[OWL_DEBUG] Parent<id=1> willUnmount",
"[OWL_DEBUG] Child<id=2> willUnmount"
]);
console.log = log;
});
-173
View File
@@ -1,173 +0,0 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
template = "Counter";
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
template = "Message";
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
template = "App";
mounted() {
this.log(
`Benchmarking Owl v${owl._version} (build date: ${
owl._date
})`
);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(data) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.10.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="'log'"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-170
View File
@@ -1,170 +0,0 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(
`Benchmarking Owl v${owl.__info__.version} (build date: ${
owl.__info__.date
})`
);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this.benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(data) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.14.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-161
View File
@@ -1,161 +0,0 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.16.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
-173
View File
@@ -1,173 +0,0 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
template = "Counter";
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
template = "Message";
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
template = "App";
mounted() {
this.log(
`Benchmarking Owl v${owl._version} (build date: ${
owl._date
})`
);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(data) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.8.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl-0.8.0.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="'log'"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
@@ -1,10 +1,11 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
state = useState({ counter: 0 });
increment() {
this.state.counter++;
@@ -15,8 +16,6 @@ class Counter extends owl.Component {
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
@@ -26,13 +25,14 @@ class Message extends owl.Component {
});
}
}
Message.components = { Counter };
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log");
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
@@ -108,10 +108,10 @@ class App extends owl.Component {
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
for (let i = 0; i < messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
messages[i] = msg;
}
});
}
@@ -129,32 +129,25 @@ class App extends owl.Component {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
this.logRef.el.appendChild(div);
this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
this.logRef.el.innerHTML = "";
}
}
App.components = { Message };
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
const templates = await owl.utils.loadFile("templates.xml");
App.env = {
qweb: new owl.QWeb({ templates })
};
const app = new App(env);
const app = new App();
app.mount(document.body);
}
@@ -2,12 +2,11 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.12.0 Benchmark</title>
<title>OWL v1.0.0-beta1 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -12,11 +12,11 @@
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<input type="checkbox" id="multipleflag" t-model="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<input type="checkbox" id="clearFlag" t-model="state.clearAfterFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
+56 -39
View File
@@ -5,15 +5,7 @@
* to log lot of helpful information on how Owl components behave.
*/
let debugSetup = {
// componentBlackList: /App/, // regexp
// componentWhiteList: /SomeComponent/, // regexp
// methodBlackList: ["mounted"], // list of method names
// methodWhiteList: ["willStart"], // list of method names
logScheduler: true, // display/mute scheduler logs
logStore: true // display/mute store logs
};
{
function debugOwl(owl, options) {
let prefix = "[OWL_DEBUG]";
let current;
Object.defineProperty(owl.Component, "current", {
@@ -23,10 +15,10 @@ let debugSetup = {
set(comp) {
current = comp;
const name = comp.constructor.name;
if (debugSetup.componentBlackList && debugSetup.componentBlackList.test(name)) {
if (options.componentBlackList && options.componentBlackList.test(name)) {
return;
}
if (debugSetup.componentWhiteList && !debugSetup.componentWhiteList.test(name)) {
if (options.componentWhiteList && !options.componentWhiteList.test(name)) {
return;
}
let __owl__;
@@ -43,7 +35,12 @@ let debugSetup = {
});
function toStr(obj) {
let str = JSON.stringify(obj || {});
let str;
try {
str = JSON.stringify(obj || {});
} catch (e) {
str = "<JSON error>";
}
if (str.length > 200) {
str = str.slice(0, 200) + "...";
}
@@ -52,82 +49,88 @@ let debugSetup = {
function debugComponent(component, name, id) {
let fullName = `${name}<id=${id}>`;
let log = str => console.log(`${prefix} ${fullName} ${str}`);
let shouldDebug = method => {
if (debugSetup.methodBlackList && debugSetup.methodBlackList.includes(method)) {
if (options.methodBlackList && options.methodBlackList.includes(method)) {
return false;
}
if (debugSetup.methodWhiteList && !debugSetup.methodWhiteList.includes(method)) {
if (options.methodWhiteList && !options.methodWhiteList.includes(method)) {
return false;
}
return true;
};
if (shouldDebug("constructor")) {
console.log(`${prefix} ${fullName} constructor, props=${toStr(component.props)}`);
log(`constructor, props=${toStr(component.props)}`);
}
if (shouldDebug("willStart")) {
owl.hooks.onWillStart(() => {
console.log(`${prefix} ${fullName} willStart`);
log(`willStart`);
});
}
if (shouldDebug("mounted")) {
owl.hooks.onMounted(() => {
console.log(`${prefix} ${fullName} mounted`);
log(`mounted`);
});
}
if (shouldDebug("willUpdateProps")) {
owl.hooks.onWillUpdateProps(nextProps => {
console.log(`${prefix} ${fullName} willUpdateProps, nextprops=${toStr(nextProps)}`);
log(`willUpdateProps, nextprops=${toStr(nextProps)}`);
});
}
if (shouldDebug("willPatch")) {
owl.hooks.onWillPatch(() => {
console.log(`${prefix} ${fullName} willPatch`);
log(`willPatch`);
});
}
if (shouldDebug("patched")) {
owl.hooks.onPatched(() => {
console.log(`${prefix} ${fullName} patched`);
log(`patched`);
});
}
if (shouldDebug("willUnmount")) {
owl.hooks.onWillUnmount(() => {
console.log(`${prefix} ${fullName} willUnmount`);
log(`willUnmount`);
});
}
const __render = component.__render.bind(component);
component.__render = function(...args) {
console.log(`${prefix} ${fullName} rendering template`);
log(`rendering template`);
__render(...args);
};
const render = component.render.bind(component);
component.render = function(...args) {
console.log(`${prefix} ${fullName} render`);
const __owl__ = component.__owl__;
let msg = `render`;
if (!__owl__.isMounted && !__owl__.currentFiber) {
msg += ` (warning: component is not mounted, this render has no effect)`;
}
log(msg);
return render(...args);
};
const mount = component.mount.bind(component);
component.mount = function(...args) {
console.log(`${prefix} ${fullName} mount`);
log(`mount`);
return mount(...args);
};
}
if (debugSetup.logScheduler) {
let isRunning;
Object.defineProperty(owl.Component.scheduler, "isRunning", {
get() {
return isRunning;
},
set(val) {
if (val) {
console.log(`${prefix} scheduler: start running tasks queue`);
} else {
console.log(`${prefix} scheduler: stop running tasks queue`);
}
isRunning = val;
if (options.logScheduler) {
let start = owl.Component.scheduler.start;
let stop = owl.Component.scheduler.stop;
owl.Component.scheduler.start = function () {
if (!this.isRunning) {
console.log(`${prefix} scheduler: start running tasks queue`);
}
});
start.call(this);
};
owl.Component.scheduler.stop = function () {
if (this.isRunning) {
console.log(`${prefix} scheduler: stop running tasks queue`);
}
stop.call(this);
};
}
if (debugSetup.logStore) {
if (options.logStore) {
let dispatch = owl.Store.prototype.dispatch;
owl.Store.prototype.dispatch = function(action, ...payload) {
console.log(`${prefix} store: action '${action}' dispatched. Payload: '${toStr(payload)}'`);
@@ -135,3 +138,17 @@ let debugSetup = {
};
}
}
// This debug function can then be used like this:
//
// debugOwl(owl, {
// componentBlackList: /App/, // regexp
// componentWhiteList: /SomeComponent/, // regexp
// methodBlackList: ["mounted"], // list of method names
// methodWhiteList: ["willStart"], // list of method names
// logScheduler: true, // display/mute scheduler logs
// logStore: true // display/mute store logs
// });
module.exports.debugOwl = debugOwl;
+1 -5
View File
@@ -28,19 +28,15 @@
<div class="benchmarks">
<ul>
<li><a href="benchmarks/owl-0.7.0">OWL 0.7.0</a></li>
<li><a href="benchmarks/owl-0.8.0">OWL 0.8.0</a></li>
<li><a href="benchmarks/owl-0.9.0">OWL 0.9.0</a></li>
<li><a href="benchmarks/owl-0.10.0">OWL 0.10.0</a></li>
<li><a href="benchmarks/owl-0.11.0">OWL 0.11.0</a></li>
<li><a href="benchmarks/owl-0.12.0">OWL 0.12.0</a></li>
<li><a href="benchmarks/owl-0.13.0">OWL 0.13.0</a></li>
<li><a href="benchmarks/owl-0.14.0">OWL 0.14.0</a></li>
<li><a href="benchmarks/owl-0.15.0">OWL 0.15.0</a></li>
<li><a href="benchmarks/owl-0.16.0">OWL 0.16.0</a></li>
<li><a href="benchmarks/owl-0.17.0">OWL 0.17.0</a></li>
<li><a href="benchmarks/owl-0.18.0">OWL 0.18.0</a></li>
<li><a href="benchmarks/owl-0.21.0">OWL 0.21.0</a></li>
<li><a href="benchmarks/owl-0.24.0">OWL 0.24.0</a></li>
<li><a href="benchmarks/owl-1.0.0-beta1">OWL 1.0.0-beta1</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul>
<ul>
+121 -10
View File
@@ -455,7 +455,7 @@ const actions = {
todo.completed = !todo.completed;
},
clearCompleted({ state, dispatch }) {
for (let todo of state.todos) {
for (let todo of state.todos.slice()) {
if (todo.completed) {
dispatch("removeTodo", todo.id);
}
@@ -1089,20 +1089,20 @@ const RESPONSIVE_XML = `<templates>
<button>Button!</button>
</div>
<t t-name="maincontent">
<FormView />
<Chatter />
</t>
<div t-name="App" class="app" t-att-class="{mobile: env.isMobile, desktop: !env.isMobile}">
<t t-set="maincontent">
<FormView />
<Chatter />
</t>
<Navbar/>
<ControlPanel/>
<div class="content-wrapper" t-if="!env.isMobile">
<div class="content">
<t t-raw="maincontent"/>
<t t-call="maincontent"/>
</div>
</div>
<t t-else="1">
<t t-raw="maincontent"/>
<t t-else="">
<t t-call="maincontent"/>
</t>
</div>
</templates>
@@ -1438,12 +1438,117 @@ const FORM_XML = `<templates>
<div>Text: <t t-esc="state.text"/></div>
<div>Other Text: <t t-esc="state.othertext"/></div>
<div>Number: <t t-esc="state.number"/></div>
<div>Boolean: <t t-if="state.bool">True</t><t t-else="1">False</t></div>
<div>Boolean: <t t-if="state.bool">True</t><t t-else="">False</t></div>
<div>Color: <t t-esc="state.color"/></div>
</div>
</templates>
`;
const PORTAL_COMPONENTS = `
// This shows the expected use case of Portal
// which is to implement something similar
// to bootstrap modal
const { Component, useState } = owl;
const { Portal } = owl.misc;
class Modal extends Component {}
Modal.components = { Portal };
class Dialog extends Component {}
Dialog.components = { Modal };
class Interstellar extends Component {}
// Main root component
class App extends Component {
state = useState({
name: 'Portal used for Dialog (Modal)',
dialog: false,
text: 'Hello !',
});
}
App.components = { Dialog , Interstellar };
// Application setup
const app = new App();
app.mount(document.body);
`;
const PORTAL_XML = `
<templates>
<t t-name="Modal">
<Portal target="'body'">
<div class="owl-modal-supercontainer">
<div class="owl-modal-backdrop"></div>
<div class="owl-modal-container">
<t t-slot="default" />
</div>
</div>
</Portal>
</t>
<t t-name="Dialog">
<Modal>
<div class="owl-dialog-body">
<t t-slot="default" />
</div>
</Modal>
</t>
<div t-name="Interstellar" class="owl-interstellar">
<h4>This is a subComponent</h4>
<p>The events it triggers will go through the Portal and be teleported
on the other side of the wormhole it has created</p>
<button t-on-click="trigger('collapse-all')">Close the wormhole</button>
</div>
<div t-name="App" t-on-collapse-all="state.dialog=false">
<div t-esc="state.name"/>
<button t-on-click="state.dialog = true">Open Dialog</button>
<Dialog t-if="state.dialog">
<div t-esc="state.text"/>
<Interstellar />
</Dialog>
</div>
</templates>
`;
const PORTAL_CSS = `
.owl-modal-supercontainer {
position: static;
}
.owl-modal-backdrop {
position: fixed;
top: 0;
left:0;
background-color: #000000;
opacity: 0.5;
width: 100vw;
height: 100vh;
z-index: 1000;
}
.owl-modal-container {
opacity:1;
z-index: 1050;
position: fixed;
top: 0;
left:0;
width: 100%;
height: 100%;
}
.owl-dialog-body {
max-width: 500px;
margin: 0 auto;
position: relative;
text-align: center;
padding: 2rem;
background-color: #FFFFFF;
max-height: 100%;
}
.owl-interstellar {
border: groove;
}`
const WMS = `// This example is slightly more complex than usual. We demonstrate
// here a way to manage sub windows in Owl, declaratively. This is still just a
// demonstration. Managing windows can be as complex as we want. For example,
@@ -1770,5 +1875,11 @@ export const SAMPLES = [
code: ASYNC_COMPONENTS,
xml: ASYNC_COMPONENTS_XML,
css: ASYNC_COMPONENTS_CSS
}
},
{
description: "Portal (Dialog)",
code: PORTAL_COMPONENTS,
xml: PORTAL_XML,
css: PORTAL_CSS,
},
];
+2 -1
View File
@@ -13,7 +13,8 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strictPropertyInitialization": true,
"strictNullChecks": true
"strictNullChecks": true,
"declarationDir": "dist/types"
},
"include": ["src/**/*.ts","src/*.ts"]
}