Compare commits

..

3 Commits

Author SHA1 Message Date
Samuel Degueldre 71a9b21472 temp 2021-10-14 13:54:16 +02:00
Samuel Degueldre 5cbf43110f [REF] qweb: use native Node and Element instead of custom Dom types 2021-10-14 13:19:31 +02:00
Géry Debongnie e00be61b8e [REF] initial prototype of owl 2 2021-10-13 16:36:59 +02:00
259 changed files with 77280 additions and 29143 deletions
+1 -1
View File
@@ -24,4 +24,4 @@ jobs:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: npm install - run: npm install
- run: npm run test - run: npm run test
- run: npm run check-formatting - run: npm run prettier
-1
View File
@@ -30,5 +30,4 @@ release-notes.md
.rpt2_cache .rpt2_cache
# useful in some cases
/temp /temp
-612
View File
@@ -1,612 +0,0 @@
# Changelog
This document contains an overview of all changes between Owl 1.x and
Owl 2.x, with some pointers on how to update the code.
Note that some of these changes can be magically implemented (for example, by
patching the `setup` method of `Component` to auto register all the lifecycle
methods as hooks). This will be done for the transition period, but will be
removed after.
## Changes
**Components**
- components can now have empty content or multiple root nodes (htmlelement or text) ([details](#31-components-can-now-have-arbitrary-content))
- new `useEffect` hook
- new `onDestroyed`, `onWillRender` and `onRendered` hooks
- breaking: lifecycle methods are removed ([details](#1-component-lifecycle-methods-are-removed))
- breaking: can no longer be mounted on detached DOM ([details](#2-components-can-no-longer-be-mounted-in-a-detached-dom-element))
- breaking: standalone `mount` method API is simpler ([details](#4-mount-method-api-is-simpler))
- breaking: components can no longer be instantiated and mounted by hand ([details](#5-components-can-no-longer-be-instantiated-and-mounted-by-hand))
- breaking: components can no longer be unmounted/remounted ([details](#6-components-can-no-longer-be-unmountedremounted))
- breaking: template name is no longer inferred from the class name ([details](#7-template-name-is-no-longer-inferred-from-the-class-name))
- breaking: components no longer have a `shouldUpdate` method ([details](#8-components-no-longer-have-a-shouldupdate-method))
- breaking: component.el may be a text node, and is no longer `null` ([details](#9-componentel-may-be-a-text-node-and-is-no-longer-null))
- breaking: style/class on components are now regular props ([details](#10-styleclass-on-components-are-now-regular-props))
- breaking: components can no longer be mounted with position=self ([details](#11-components-can-no-longer-be-mounted-with-positionself))
- breaking: `t-on` does not work on components any more ([details](#12-t-on-does-not-work-on-components-any-more))
- breaking: `t-component` no longer accepts strings ([details](#17-t-component-no-longer-accepts-strings))
**Portal**
- portals can now have arbitrary content (no longer restricted to one single child)
- breaking: does no longer transfer dom events ([details](#13-portal-does-no-longer-transfer-dom-events))
- breaking: does render as an empty text node instead of `<portal/>` ([details](#14-portal-does-render-as-an-empty-text-node-instead-of-portal))
**Slots**
- breaking: `t-set` does not define a slot any more ([details](#3-t-set-will-no-longer-work-to-define-a-slot))
**Miscellaneous**
- improved performance
- much simpler code
- finer grained reactivity: owl 2 tracks change per key/component
- finer grained reactivity: sub components can reobserve state
- new App class to encapsulate a root Owl component (with the config for that application)
- new `Memo` component
- breaking: `Context` is removed ([details](#15-context-is-removed))
- breaking: `env` is now totally empty ([details](#16-env-is-now-totally-empty))
- breaking: most exports are exported at top level ([details](#18-most-exports-are-exported-at-top-level))
- breaking: properties are no longer set as attributes ([details](#19-properties-are-no-longer-set-as-attributes))
- breaking: `t-foreach` should always have a corresponding `t-key` ([details](#20-t-foreach-should-always-have-a-corresponding-t-key))
- breaking: `EventBus` api changed: it is now an `EventTarget` ([details](#21-eventbus-api-changed-it-is-now-an-eventtarget))
- breaking: `Store` is removed ([details](#22-store-is-removed))
- breaking: `Router` is removed ([details](#23-router-is-removed))
- breaking: transition system is removed ([details](#24-transition-system-is-removed))
- breaking: no more global components or templates ([details](#25-no-more-global-components-or-templates))
- breaking: `AsyncRoot` utility component is removed ([details](#26-asyncroot-utility-component-is-removed))
- breaking: `useSubEnv` only applies to child components ([details](#27-usesubenv-only-applies-to-child-components))
- breaking: `env` is now frozen ([details](#28-env-is-now-frozen))
- breaking: `t-ref` does not work on components ([details](#29-t-ref-does-not-work-on-component))
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
- breaking: `renderToString` function on qweb has been removed ([details](#32-rendertostring-on-qweb-has-been-removed))
## Details/Rationale/Migration
All changes are listed in no particular order.
### 1. component lifecycle methods are removed
There was two ways to define hooks: the component methods (`willStart`, `mounted`, ...) and the hooks (`onWillStart`, `onMounted`, ...). In Owl 2, the component methods have been removed.
Rationale: it makes the implementation simpler and slightly faster. Hooks are more composable
than component methods. It enforces a single entry point to check all the useful lifecycle
calls (instead of it being scattered in the component definition). It feels more "modern".
Migration: lifecycle methods should be defined in the `setup`:
```js
class MyComponent extends Component {
mounted() {
// do something
}
}
```
should become:
```js
class MyComponent extends Component {
setup() {
onMounted(() => {
// do something
});
}
}
```
### 2. components can no longer be mounted in a detached dom element
Nor document fragment.
Rationale: it is actually very difficult to do it: this implies that a component
can be mounted more than once, that we need to check every time different status,
that some elements is in the dom, and was a cause for bugs. Also, we don't use it
in practice. Removing this means that we have a much simpler mental model of what
happens.
Migration: well, not really easy. The code needs to be refactored in a different way.
### 3. **`t-set` will no longer work to define a slot**
The `t-set` directive cannot define a slot anymore. Only the `t-set-slot` directive
can do it.
Rationale: it was left for compatibility reason, but was deprecated anyway.
Migration: `t-set` should be changed to `t-set-slot` (when defining a slot)
Example:
```xml
<SideBar><t t-set="content">content</t></SideBar>
```
should become:
```xml
<SideBar><t t-set-slot="content">content</t></SideBar>
```
### 4. `mount` method API is simpler
Before, the `mount` method was used like this:
```js
await mount(Root, { target: document.body });
```
It is now simpler and takes the root component and a target argument:
```js
await mount(Root, document.body);
```
Rationale: the `mount` method is only useful anyway for small toy examples,
because real applications will need to configure the templates, the translations,
and other stuff. All complex usecases need to go through the new `App` class,
that encapsulates the root of an owl application.
### 5. components can no longer be instantiated and mounted by hand
In Owl 1, it was possible to instantiate a component by hand:
```js
const root = new Root();
await root.mount(document.body);
```
Now, it is no longer possible. All component instantiations should be done by
the owl framework itself.
Rationale: the `mount` method does not make sense for all non root components.
Also, the fact that it was possible for a component to be sometimes root,
sometimes a child made for a weird constructor signature. This changes makes it
simpler.
Migration: all code doing that should use either the `mount` method (if the use
case is simple enough, or the `App` class):
```js
const app = new App(Root);
app.configure({ templates: ..., ...});
await app.mount(document.body);
```
### 6. components can no longer be unmounted/remounted
Rationale: this is a very difficult feature to implement (it adds a lot of possible
state transitions), compared to its benefit.
Migration: all code using it should find a way to export and reimport the state
### 7. template name is no longer inferred from the class name
Before, it was possible to define a component without specifying its template:
```js
class Blabla extends Component {
// no static template here!
}
```
with the `Blabla` template. It also worked with subclasses. But then, this means
that the code had to look up all the super classes names to find the correct
template.
Rationale: in practice, it is not really useful, since all templates are usually
namespaced: `web.SomeComponent` anyway. All the trouble to do that was just not
worth it.
Migration: simply explicitely defines the template key everytime:
```js
class Blabla extends Component {}
Blabla.template = "Blabla";
```
### 8. components no longer have a `shouldUpdate` method
Rationale: `shouldUpdate` is a dangerous method to use, that may cause a lot of
issues. Vue does not have such a mechanism (see https://github.com/vuejs/vue/issues/4255),
because the reactivity system in Vue is smart enough to only rerender the minimal
subset of components that is subscribed to a piece of state. Now, Owl 2 features
a much more powerful reactivity system.
Migration code: remove the `shouldUpdate` methods. Then, maybe the following
ideas may help:
- try to organize the state/architecture to minimize the number of state updates
- take advantage of the finer reactivity system. For example, if we have a list
of items, with a component for each item, we can write this:
```js
class Item extends Component {
setup() {
this.item = useState(this.props.item); // and only use this, not props.item
}
}
```
Doing so will make it that each `Item` component will register itself as an
observer of its own item, and will be the only component being rerendered when
its item object is updated.
- use the `Memo` component to wrap some piece of template. `Memo` memoize its
content, and only update itself if its props are different (shallow comparison):
```xml
<Memo a="state.a" b="state.b">
<t t-esc="state.a"/>
<t t-esc="state.b"/>
<t t-esc="state.c"/>
</Memo>
```
### 9. component.el may be a text node, and is no longer `null`
This comes from the fact that Owl 2 supports fragments (arbitrary content). When
it is not defined, it was `null` in Owl 1 and is `undefined` in owl 2.
### 10. style/class on components are now regular props
Before, it was possible to do this in a template:
```xml
<Child style="..." class="..."/>
```
(or with `t-att-style` and `t-att-class`). This does no longer work, as they are
now considered normal props.
Rationale: with the move to fragments, the semantics of where the style/class
attribute should be set is unclear. Also, it is actually very hard to implement
properly, in particular with higher order components. And another issue is that
it (slightly) breaks the encapsulation of behaviour from the `Child` component
perspective.
Migration: each component that wishes to be customized should explicitely add
the `class` and `style` attributes in its template. Also, the parent component
should be aware that since we are talking about props, it should be a javascript expression:
In parent:
```xml
<Child class="'o_my_god'"/>
```
and in child:
```xml
<div t-att-class="props.class">
...
</div>
```
### 11. components can no longer be mounted with position=self
Rationale: this is due to the implementation of owl 2 virtual dom. The hack
necessary to support position=self does not work. This position also is not
compatible with the fact that a component can have a root `<div>` then later,
change it to something else, or even a text node.
Migration: no real way to do the same. Owl application needs to be appended or
prepended in something, maybe a `div`.
### 12. `t-on` does not work on components any more
In owl 1, it was possible to bind an event listener on a component tag in a
template:
```xml
<SomeComponent t-on-some-event="doSomething"/>
```
This does not work any more.
Rationale: with the support of fragments, there is no longer a canonical html
element that we can refer. So, this makes it difficult to implement correctly
and efficiently. Also, we noticed in practice that the event system was an issue
in some cases, when components need to communicate before they are mounted. In
those cases, the better solution is to directly use a callback. Also, another
conceptual issue with this is that it kind of breaks the component encapsulation.
The child component kind of leak its own implementation to the outside world.
Migration: a quick fix that may work in some cases is to simply bind the event
handler on a parent htmlelement. A better way to do it, if possible, is to change
the component API to accept explicitely a callback as props.
```xml
<SomeComponent onSomeEvent="doSomething"/>
```
### 13. Portal does no longer transfer DOM events
In Owl 1, a Portal component would listen to events emitted on its portalled
child, and redispatch them on itself. It no longer works.
Rationale: Portal now supports an arbitrary content (so, more than one child,
and potentially no html element), so it is already unclear what it should listen
to. Also, redispatching events was an hack. And this changes allows the portal
to render itself as a text node, which is nice. This is also in line with the
fact that modern Owl moves toward using callback instead of `t-on` for communication.
Migration: use callback if possible to communicate. Otherwise, use a sub env.
### 14. Portal does render as an empty text node instead of `<portal/>`
That is pretty nice. No real migration needed.
### 15. Context is removed
Context was an abstraction in Owl that was used to define some reactive state
and to let some components subscribe to it, then only them would be rerendered
if the context was updated. This has been removed.
Rationale: first, the Context api and code was kind of awkward, which is a sign
that the abstraction is not well thought. But the good news is that it is actually
completely replaced by the new reactivity system, which is even more powerful,
since it can tracks changes key by key.
Migration: replace all uses of Context with the new reactivity system.
```js
// somewhere, maybe in a service, or in the global env
const context = observe({some: "state"})
// in a component that would previously get a reference to the context:
setup() {
this.context = useState(context);
// now the component is subscribed to the context and will react to any
// change for any key read by the component, and only those changes
}
```
### 16. `env` is now totally empty
In Owl 1, the `env` object had to contain a QWeb instance. This was the way
components would get a reference to their template function. It no longer works
that way: the `env` object is now totally empty (from the perspective of Owl).
It is now a user space concept, useful for the application.
Rationale: first, there is no longer a QWeb class. Also, this changes simplifies
the way components works internally.
Migration: there is no proper way to get an equivalent. The closest is to get
a reference to the root App using `this.__owl__.app`. If you need to do this,
let us know. If this is a legitimate usecase, we may add a `useApp` hook.
### 17. `t-component` no longer accepts strings
In owl 1, we could write this:
```xml
<t t-component="Coucou"/>
```
This meant that Owl would look for the component class like this: `components["Coucou"]`,
so, essentially equivalent to `<Coucou/>`. In Owl 2, the `t-component` directive
is assumed to be an expression evaluating to a component class:
```js
class Parent extends Component {
static template = xml`<t t-component="Child"/>`;
Child = Child;
}
```
Rationale: it simply seems more consistent with the way directive works. Also,
the implementation is slightly simpler.
Migration: simply using `constructor.components.Coucou` instead of `Coucou` will
do the trick.
### 18. most exports are exported at top level
Most exports are flattened: for ex, `onMounted` is in owl, not in `owl.hooks`.
Rationale: this makes it easier to work with, instead of importing stuff from
`owl`, then `owl.hooks` and `owl.tags` for example.
Migration: all import code simply need to be slightly adapted.
### 19. Properties are no longer set as attributes
Formerly, html properties `<input type="checkbox" t-att-checked="blah"/>` were
set as property and as attribute, so, they would be visible in the DOM:
`<input type="checkbox" checked="blah"/>`. Now, they are treated as property only:
`<input type="checkbox"/>`.
Rationale: this is actually simple to do, is faster, and makes more sense to me.
### 20. `t-foreach` should always have a corresponding `t-key`
It was possible in Owl 1 to write a `t-foreach` without a `t-key`. In that case,
the index was used as key. Since it was clearly a possible bug, Owl 1 had a
warning in some cases, when it could detect that there was definitely not a `t-key`.
However, this was imperfect, and in some cases no warning was displayed. In Owl 2,
the tag with a `t-foreach` has to have a corresponding `t-key`.
Rationale: this makes it easier to avoid bugs.
Migration: simply move the `t-key` to the tag with the `t-foreach`. If this is
a situation where there is really not a need for a `t-key`, you can still add
it with the `_index` suffix:
```xml
<div t-foreach="items" t-as="item" t-key="item_index">
...
</div>
```
### 21. `EventBus` api changed: it is now an `EventTarget`
In Owl 1, the `EventBus` class was done manually, with a custom API. In Owl 2,
it simply extends `EventTarget` (the native Dom class), so its implementation
is basically only 5 lines long. This means that it has now the usual DOM interface:
```js
bus.addEventListener('event-name', callback);
```
Rationale: it makes it easier to have just one interface to remember, it makes
the code simpler
Migration: most bus methods need to be adapted. So, `bus.on("event-type", owner, (info) => {...})` has to be
rewritten like this: `bus.addEventListener("event-type", (({detail: info}) => {...}).bind(owner))`.
Do not forget to similarly replace `bus.off(...)` by `bus.removeEventListener(...)`
### 22. `Store` is removed
The Store system had been abandoned in owl 2.
Rationale: first, it was complicated to maintain. Second, it was not really
used in Odoo. Finally, the new reactivity system seems to be a pretty good basis
to write a store, and it should not take much work. Also, this can be done in
user space (so, not necessarily at the framework level). Another point is that
the store API was invented before the hooks, then was still a little awkward.
Migration:
- rewrite the code not to use a store
- probably use the reactivity system instead and build a store class and a few
hooks on top of it.
### 23. `Router` is removed
Rationale: Router was not used that much, and it felt like it did not fit in Owl 2.
Its API needs to be reworked, and we are not confident that it is a good
experience to use it. Also, it can be done in userspace (it does not need specific
integration at the framework level)
Migration: reimport all missing piece from the code in Owl 1.
### 24. transition system is removed
Rationale: this was a high ratio cost/value, with a lot of potential for bugs.
We feel like there should be a way to reimplement in userspace the simple cases.
Maybe something like: add a `t-ref` in the template, and define a hook `useFadeOut`
that takes the ref, and add a fadeout class at initial render, then in mounted,
wait for a micro tick and remove it.
Migration: try to reimplement it manually.
### 25. no more global components or templates
It was possible in Owl 1 to register globally a component or a template. This is
no longer the case in Owl 2.
Rationale: first, this was a tradeoff: ease of use was gained, but at the cost
of a higher complexity. Users had to know that there was a magic mechanism. Also,
it was not used much in practice, and the cost of having to import manually components
is low. Finally, this can be mostly done in user space (for example, by subclassing
`Component`).
Migration: import manually all required global components, or find a way to organize
the code to do it.
### 26. `AsyncRoot` utility component is removed
Rationale: it was difficult to understand, never used, and not really useful.
It seems better to control the asynchrony of an application by simply controlling
how/when the state is updated, and how each component is loading/updating itself.
Migration: remove the `AsyncRoot` component, then possibly, reorganize the code
to fetch data in a higher order component, and using a `t-if/t-else` to display
either a fallback when the data is not ready, or the actual component with data
as props. If there is no escape, and `AsyncRoot` is needed, please reach out to
us so we can study this usecase.
### 27. `useSubEnv` only applies to child components
In Owl 1, a call to `useSubEnv` would define a new environment for the children
AND the component. It now only defines an environment for the children.
Rationale: This was a subtle cause for bugs: some code had to be rrun
before the call to `useSubEnv`, otherwise it could interfere with the sub environment.
### 28. `env` is now frozen
In Owl 2, the `env` object is frozen. It can no longer be modified (structurally)
arbitrarily.
Rationale: it seems like the `env` object purpose is to have a global channel of
communication between components. It is however scary if anyone can add something
to it. The usual use case is to add something to the environment for some child
components. This use case still works with `useSubEnv`.
Migration: use `useSubEnv` instead of writing directly to the env. Also, note
that the environment given to the App can initially contain anything.
### 29. `t-ref` does not work on component
Before, `t-ref` could be used to get a reference to a child component. It no
longer works.
Rationale: the possibility of having a ref to a child component breaks the
encapsulation provided by Owl components: a child component now has a private
and a public interface. Another issue is that it may be unclear when the ref
should be set: is the component active on setup, or on mounted? Also, it is
kind of awkward to implement.
Migration: the `env` and `props` should provide a communication channel wide enough:
the sub component can expose its public API by calling a callback at the proper
timing, or by triggering an event.
### 30. `t-on` does not accept expressions, only functions
In Owl 1, it was possible to define simple expressions inline, in a template:
```xml
<button t-on-click="state.value = state.value + 1">blabla</button>
<button t-on-click="someFunction(someVar)">blabla</button>
```
This does not work anymore. Now, the `t-on` directive assumes that what it get is
a function.
Rationale: the fact that owl 1 had to support expressions meant that it was not
possible to properly inject the event in general. With this restriction, Owl 2
can support more general use cases. Also, the examples above can simply be
wrapped in a lambda function.
Migration: use lambda functions. For example, the two examples above can be
adapted like this:
```xml
<button t-on-click="() => state.value = state.value + 1">blabla</button>
<button t-on-click="() => this.someFunction(someVar)">blabla</button>
```
### 31. components can now have arbitrary content
Before Owl 2, components had to limit themselves to one single htmlelement as
root. Now, the content is arbitrary: it can be empty, or multiple html elements.
So, the following template works for components:
```xml
<div>1</div>
<div>2</div>
hello
```
### 32. `renderToString` on QWeb has been removed
Rationale: the `renderToString` function was a qweb method, which made sense because
the qweb instance knew all templates. But now, the closest analogy is the `App`
class, but it is not as convenient, since the `app` instance is no longer visible
to components (while before, `qweb` was in the environment).
Also, this can easily be done in userspace, by mounting a component in a div. For example:
```js
export async function renderToString(template, context) {
class C extends Component {
static template = template;
}
const div = document.createElement('div');
document.body.appendChild(div);
const component = await mount(C, div);
const result = div.innerHTML;
app.destroy();
div.remove();
return result;
}
+58 -56
View File
@@ -1,4 +1,4 @@
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Owl Framework</a> 🦉</h1> <h1 align="center">🦉 <a href="https://odoo.github.io/owl/">OWL Framework</a> 🦉</h1>
[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) [![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
[![npm version](https://badge.fury.io/js/@odoo%2Fowl.svg)](https://badge.fury.io/js/@odoo%2Fowl) [![npm version](https://badge.fury.io/js/@odoo%2Fowl.svg)](https://badge.fury.io/js/@odoo%2Fowl)
@@ -6,39 +6,51 @@
_Class based components with hooks, reactive state and concurrent mode_ _Class based components with hooks, reactive state and concurrent mode_
**Try it online!** you can experiment with the Owl framework in an online [playground](https://odoo.github.io/owl/playground).
## Project Overview ## Project Overview
The Odoo Web Library (Owl) is a smallish (~<20kb gzipped) UI framework built by The Odoo Web Library (OWL) is a smallish (~<20kb gzipped) UI framework intended to
[Odoo](https://www.odoo.com/) for its products. Owl is a modern be the basis for the [Odoo](https://www.odoo.com/) Web Client. Owl is a modern
framework, written in Typescript, taking the best ideas from React and Vue in a framework, written in Typescript, taking the best ideas from React and Vue in a
simple and consistent way. Owl's main features are: simple and consistent way. Owl's main features are:
- a declarative component system, - a declarative component system,
- a reactivity system based on hooks, - a reactivity system based on hooks,
- concurrent mode by default, - concurrent mode by default,
- a store and a frontend router
Owl components are defined with ES6 classes and xml templates, uses an Owl components are defined with ES6 classes, they use QWeb templates, an
underlying virtual DOM, integrates beautifully with hooks, and the rendering is underlying virtual DOM, integrates beautifully with hooks, and the rendering is
asynchronous. asynchronous.
Quick links: **Try it online!** An online playground is available at
[https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground)
to let you experiment with the Owl framework. There are some code examples to
showcase some interesting features.
- [documentation](#documentation), Owl is currently stable. Possible future changes are explained in the
- [changelog](CHANGELOG.md) (from Owl 1.x to 2.x), [roadmap](roadmap.md).
- [playground](https://odoo.github.io/owl/playground)
## Why Owl?
Why did Odoo decide to make Yet Another Framework? This is really a question
that deserves [a long answer](doc/miscellaneous/why_owl.md). But in short, we believe that
while the current state of the art frameworks are excellent, they are not
optimized for our use case, and there is still room for something else.
If you are interested in a comparison with React or Vue, you will
find some more additional information [here](doc/miscellaneous/comparison.md).
## Example ## Example
Here is a short example to illustrate interactive components: Here is a short example to illustrate interactive components:
```javascript ```javascript
const { Component, useState, mount, xml } = owl; const { Component, useState } = owl;
const { xml } = owl.tags;
class Counter extends Component { class Counter extends Component {
static template = xml` static template = xml`
<button t-on-click="() => state.value++"> <button t-on-click="state.value++">
Click Me! [<t t-esc="state.value"/>] Click Me! [<t t-esc="state.value"/>]
</button>`; </button>`;
@@ -55,7 +67,8 @@ class App extends Component {
static components = { Counter }; static components = { Counter };
} }
mount(App, document.body); const app = new App();
app.mount(document.body);
``` ```
Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate). Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
@@ -65,55 +78,41 @@ But this is not mandatory, many applications will load templates separately.
More interesting examples can be found on the More interesting examples can be found on the
[playground](https://odoo.github.io/owl/playground) application. [playground](https://odoo.github.io/owl/playground) application.
## Design Principles
OWL is designed to be used in highly dynamic applications where changing
requirements are common and code needs to be maintained by large teams.
- **XML based**: templates are based on the XML format, which allows interesting
applications. For example, they could be stored in a database and modified
dynamically with `xpaths`.
- **templates compilation in the browser**: this may not be a good fit for all
applications, but if you need to generate dynamically user interfaces in the
browser, this is very powerful. For example, a generic form view component
could generate a specific form user interface for each various models, from a XML view.
- **no toolchain required**: this is extremely useful for some applications, if,
for various reasons (security/deployment/dynamic modules/specific assets tools),
it is not ok to use standard web tools based on `npm`.
Owl is not designed to be fast nor small (even though it is quite good on those
two topics). It is a no nonsense framework to build applications. There is only
one way to define components (with classes). There is no black magic. It just
works.
## Documentation ## Documentation
### Learning Owl A complete documentation for Owl can be found here:
Are you new to Owl? This is the place to start! - [Main documentation page](doc/readme.md).
- [Tutorial: create a TodoList application](doc/learning/tutorial_todoapp.md) Some of the most important pages are:
- [Tutorial: TodoList application](doc/learning/tutorial_todoapp.md)
- [How to start an Owl project](doc/learning/quick_start.md) - [How to start an Owl project](doc/learning/quick_start.md)
- [How to test Components](doc/learning/how_to_test.md) - [QWeb templating language](doc/reference/qweb_templating_language.md)
- [How to write Single File Components](doc/learning/how_to_write_sfc.md)
### Reference
You will find here a complete reference of every feature, class or object
provided by Owl.
- [Animations](doc/reference/animations.md)
- [Browser](doc/reference/browser.md)
- [Component](doc/reference/component.md) - [Component](doc/reference/component.md)
- [Content](doc/reference/content.md)
- [Concurrency Model](doc/reference/concurrency_model.md)
- [Configuration](doc/reference/config.md)
- [Context](doc/reference/context.md)
- [Environment](doc/reference/environment.md)
- [Event Bus](doc/reference/event_bus.md)
- [Event Handling](doc/reference/event_handling.md)
- [Error Handling](doc/reference/error_handling.md)
- [Hooks](doc/reference/hooks.md) - [Hooks](doc/reference/hooks.md)
- [Mounting a component](doc/reference/mounting.md)
- [Miscellaneous Components](doc/reference/misc.md)
- [Observer](doc/reference/observer.md)
- [Props](doc/reference/props.md)
- [Props Validation](doc/reference/props_validation.md)
- [QWeb Templating Language](doc/reference/qweb_templating_language.md)
- [QWeb Engine](doc/reference/qweb_engine.md)
- [Slots](doc/reference/slots.md)
- [Tags](doc/reference/tags.md)
- [Utils](doc/reference/utils.md)
### Other Topics
This section provides miscellaneous document that explains some topics
which cannot be considered either a tutorial, or reference documentation.
- [Owl architecture: the Virtual DOM](doc/miscellaneous/vdom.md)
- [Owl architecture: the rendering pipeline](doc/miscellaneous/rendering.md)
- [Comparison with React/Vue](doc/miscellaneous/comparison.md)
- [Why did Odoo built Owl?](doc/miscellaneous/why_owl.md)
## Installing Owl ## Installing Owl
@@ -126,5 +125,8 @@ npm install @odoo/owl
If you want to use a simple `<script>` tag, the last release can be downloaded here: If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.4.7](https://github.com/odoo/owl/releases/tag/v1.4.7) - [owl-1.0.13](https://github.com/odoo/owl/releases/tag/v1.0.13)
## License
OWL is [LGPL licensed](./LICENSE).
+43
View File
@@ -0,0 +1,43 @@
# 🦉 How to debug Owl applications 🦉
Non trivial applications become quickly more difficult to understand. It is then
useful to have a solid understanding of what is going on. To help with that,
logging useful information is extremely valuable. There is a [javascript file](../../tools/debug.js) which can be evaluated in an application.
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:
```
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
});
```
The above code, once pasted somewhere in the main javascript file of an owl
application, will log information looking like this:
```
[OWL_DEBUG] TodoApp<id=1> constructor, props={}
[OWL_DEBUG] TodoApp<id=1> mount
[OWL_DEBUG] TodoApp<id=1> willStart
[OWL_DEBUG] TodoApp<id=1> rendering template
[OWL_DEBUG] TodoItem<id=2> constructor, props={"id":2,"completed":false,"title":"hey"}
[OWL_DEBUG] TodoItem<id=2> willStart
[OWL_DEBUG] TodoItem<id=3> constructor, props={"id":4,"completed":false,"title":"aaa"}
[OWL_DEBUG] TodoItem<id=3> willStart
[OWL_DEBUG] TodoItem<id=2> rendering template
[OWL_DEBUG] TodoItem<id=3> rendering template
[OWL_DEBUG] TodoItem<id=3> mounted
[OWL_DEBUG] TodoItem<id=2> mounted
[OWL_DEBUG] TodoApp<id=1> mounted
```
Each component has an internal `id`, which is very useful when debugging.
Note that it is certainly useful to run this code at some point in an application,
just to get a feel of what each user action implies, for the framework.
+47 -11
View File
@@ -30,21 +30,27 @@ To help with this, it is useful to have a `helper.js` file that contains some
common utility functions: common utility functions:
```js ```js
let lastFixture = null;
export function makeTestFixture() { export function makeTestFixture() {
let fixture = document.createElement("div"); let fixture = document.createElement("div");
document.body.appendChild(fixture); document.body.appendChild(fixture);
if (lastFixture) {
lastFixture.remove();
}
lastFixture = fixture;
return fixture; return fixture;
} }
export async function nextTick() { export function nextTick() {
await new Promise((resolve) => setTimeout(resolve)); let requestAnimationFrame = owl.Component.scheduler.requestAnimationFrame;
await new Promise((resolve) => requestAnimationFrame(resolve)); return new Promise(function(resolve) {
setTimeout(() => requestAnimationFrame(() => resolve()));
});
}
export function makeTestEnv() {
// application specific. It needs a way to load actual templates
const templates = ...;
return {
qweb: new QWeb(templates),
..., // each service can be mocked here
};
} }
``` ```
@@ -53,7 +59,7 @@ With such a file, a typical test suite for Jest will look like this:
```js ```js
// in SomeComponent.test.js // in SomeComponent.test.js
import { SomeComponent } from "../../src/ui/SomeComponent"; import { SomeComponent } from "../../src/ui/SomeComponent";
import { nextTick, makeTestFixture } from '../helpers'; import { nextTick, makeTestFixture, makeTestEnv} from '../helpers';
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -64,6 +70,9 @@ let env: Env;
beforeEach(() => { beforeEach(() => {
fixture = makeTestFixture(); fixture = makeTestFixture();
env = makeTestEnv();
// we set here the default environment for each component created in the test
Component.env = env;
}); });
afterEach(() => { afterEach(() => {
@@ -76,7 +85,8 @@ afterEach(() => {
describe("SomeComponent", () => { describe("SomeComponent", () => {
test("component behaves as expected", async () => { test("component behaves as expected", async () => {
const props = {...}; // depends on the component const props = {...}; // depends on the component
const comp = await mount(SomeComponent, fixture, { props }); const comp = new SomeComponent(null, props);
await comp.mount(fixture);
// do some assertions // do some assertions
expect(...).toBe(...); expect(...).toBe(...);
@@ -93,3 +103,29 @@ describe("SomeComponent", () => {
Note that Owl does wait for the next animation frame to actually update the DOM. Note that Owl does wait for the next animation frame to actually update the DOM.
This is why it is necessary to wait with the `nextTick` (or other methods) to This is why it is necessary to wait with the `nextTick` (or other methods) to
make sure that the DOM is up-to-date. make sure that the DOM is up-to-date.
It is sometimes useful to wait until Owl is completely done updating components
(in particular, if we have a highly concurrent user interface). This next
helper simply polls every 20ms the internal Owl task queue and returns a promise
which resolves when it is empty:
```js
function afterUpdates() {
return new Promise((resolve, reject) => {
let timer = setTimeout(poll, 20);
let counter = 0;
function poll() {
counter++;
if (owl.Component.scheduler.tasks.length) {
if (counter > 10) {
reject(new Error("timeout"));
} else {
timer = setTimeout(poll);
}
} else {
resolve();
}
}
});
}
```
+133
View File
@@ -0,0 +1,133 @@
# 🦉 Quick Overview 🦉
Owl components in an application are used to define a (dynamic) tree of components.
```
Root
/ \
A B
/ \
C D
```
**State:** each component can manage its own local state. It is a simple ES6
class, there are no special rules:
```js
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = { value: 0 };
increment() {
this.state.value++;
this.render();
}
}
```
The example above shows a component with a local state. Note that since there
is nothing magical to the `state` object, we need to manually call the `render`
function whenever we update it. This can quickly become annoying (and not
efficient if we do it too much). There is a better way: using the `useState`
hook, which transforms an object into a reactive version of itself:
```js
const { useState } = owl.hooks;
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
Note that the `t-on-click` handler can even be replaced by an inline statement:
```xml
<button t-on-click="state.value++">
```
**Props:** sub components often needs some information from their parents. This
is done by adding the required information to the template. This will then be
accessible by the sub component in the `props` object. Note that there is an
important rule here: the information contained in the `props` object is not
owned by the sub component, and should never be modified.
```js
class Child extends Component {
static template = xml`<div>Hello <t t-esc="props.name"/></div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child name="'Owl'" />
<Child name="'Framework'" />
</div>`;
static components = { Child };
}
```
**Communication:** there are multiple ways to communicate information between
components. However, the two most important ways are the following:
- from parent to children: by using `props`,
- from a children to one of its parent: by triggering events.
The following example illustrate both mechanisms:
```js
class OrderLine extends Component {
static template = xml`
<div t-on-click="add">
<div><t t-esc="props.line.name"/></div>
<div>Quantity: <t t-esc="props.line.quantity"/></div>
</div>`;
add() {
this.trigger("add-to-order", { line: props.line });
}
}
class Parent extends Component {
static template = xml`
<div t-on-add-to-order="addToOrder">
<OrderLine
t-foreach="orders"
t-as="line"
line="line" />
</div>`;
static components = { OrderLine };
orders = useState([
{ id: 1, name: "Coffee", quantity: 0 },
{ id: 2, name: "Tea", quantity: 0 },
]);
addToOrder(event) {
const line = event.detail.line;
line.quantity++;
}
}
```
In this example, the `OrderLine` component trigger a `add-to-order` event. This
will generate a DOM event which will bubble along the DOM tree. It will then be
intercepted by the parent component, which will then get the line (from the
`detail` key) and then increment its quantity. See the page on [event handling](../reference/event_handling.md)
for more details on how events work.
Note that this example would have also worked if the `OrderLine` component
directly modifies the `line` object. However, this is not a good practice: this
only works because the `props` object received by the child component is reactive,
so the child component is then coupled to the parents implementation.
+52 -40
View File
@@ -36,8 +36,6 @@ hello_owl/
The file `owl.js` can be downloaded from the last release published at The file `owl.js` can be downloaded from the last release published at
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases). It [https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases). It
is a single javascript file which export all Owl into the global `owl` object. is a single javascript file which export all Owl into the global `owl` object.
Note that there are multiple files, and in this case, we need one of the two
files suffixed with `.iife`: they are built to be directly used in a browser.
Now, `index.html` should contain the following: Now, `index.html` should contain the following:
@@ -47,24 +45,31 @@ Now, `index.html` should contain the following:
<head> <head>
<title>Hello Owl</title> <title>Hello Owl</title>
<script src="owl.js"></script> <script src="owl.js"></script>
</head>
<body>
<script src="app.js"></script> <script src="app.js"></script>
</body> </head>
<body></body>
</html> </html>
``` ```
And `app.js` should look like this: And `app.js` should look like this:
```js ```js
const { Component, mount, xml } = owl; const { Component } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
// Owl Components // Owl Components
class Root extends Component { class App extends Component {
static template = xml`<div>Hello Owl</div>`; static template = xml`<div>Hello Owl</div>`;
} }
mount(Root, document.body); // Setup code
function setup() {
const app = new App();
app.mount(document.body);
}
whenReady(setup);
``` ```
Now, simply loading this html file in a browser should display a welcome message. Now, simply loading this html file in a browser should display a welcome message.
@@ -89,16 +94,14 @@ Let us start a new project with the following file structure:
``` ```
hello_owl/ hello_owl/
src/ src/
app.js
index.html index.html
main.js main.js
owl.js owl.js
root.js
``` ```
As previously, the file `owl.js` can be downloaded from the last release published at As previously, the file `owl.js` can be downloaded from the last release published at
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases). [https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases).
Note that there are multiple files, and in this case, we need one of the two
files suffixed with `.iife`: they are built to be directly used in a browser.
Now, `index.html` should contain the following: Now, `index.html` should contain the following:
@@ -108,33 +111,38 @@ Now, `index.html` should contain the following:
<head> <head>
<title>Hello Owl</title> <title>Hello Owl</title>
<script src="owl.js"></script> <script src="owl.js"></script>
</head>
<body>
<script src="main.js" type="module"></script> <script src="main.js" type="module"></script>
</body> </head>
<body></body>
</html> </html>
``` ```
Not that the `main.js` script tag has the `type="module"` attribute. This means Not that the `main.js` script tag has the `type="module"` attribute. This means
that the browser will parse the script as a module, and load all its dependencies. that the browser will parse the script as a module, and load all its dependencies.
Here is the content of `root.js` and `main.js`: Here is the content of `app.js` and `main.js`:
```js ```js
// root.js ---------------------------------------------------------------------- // app.js ----------------------------------------------------------------------
const { Component, mount, xml } = owl; const { Component } = owl;
const { xml } = owl.tags;
export class Root extends Component { export class App extends Component {
static template = xml`<div>Hello Owl</div>`; static template = xml`<div>Hello Owl</div>`;
} }
// main.js --------------------------------------------------------------------- // main.js ---------------------------------------------------------------------
import { Root } from "./root.js"; import { App } from "./app.js";
mount(Root, document.body); function setup() {
const app = new App();
app.mount(document.body);
}
owl.utils.whenReady(setup);
``` ```
The `main.js` file imports the `root.js` file. Note that the import statement has The `main.js` file import the `app.js` file. Note that the import statement has
a `.js` suffix, which is important. Most text editor can understand this syntax a `.js` suffix, which is important. Most text editor can understand this syntax
and will provide autocompletion. and will provide autocompletion.
@@ -187,11 +195,11 @@ hello_owl/
index.html index.html
src/ src/
components/ components/
Root.js App.js
main.js main.js
tests/ tests/
components/ components/
Root.test.js App.test.js
helpers.js helpers.js
.gitignore .gitignore
package.json package.json
@@ -218,15 +226,13 @@ Note that there are no `<script>` tag here. They will be injected by webpack.
Now, let's have a look at the javascript files: Now, let's have a look at the javascript files:
```js ```js
// src/components/Root.js ------------------------------------------------------- // src/components/App.js -------------------------------------------------------
import { Component, xml, useState } from "@odoo/owl"; import { Component, tags, useState } from "@odoo/owl";
export class Root extends Component { const { xml } = tags;
static template = xml`
<div t-on-click="update">
Hello <t t-esc="state.text"/>
</div>`;
export class App extends Component {
static template = xml`<div t-on-click="update">Hello <t t-esc="state.text"/></div>`;
state = useState({ text: "Owl" }); state = useState({ text: "Owl" });
update() { update() {
this.state.text = this.state.text === "Owl" ? "World" : "Owl"; this.state.text = this.state.text === "Owl" ? "World" : "Owl";
@@ -234,15 +240,19 @@ export class Root extends Component {
} }
// src/main.js ----------------------------------------------------------------- // src/main.js -----------------------------------------------------------------
import { utils, mount } from "@odoo/owl"; import { utils } from "@odoo/owl";
import { Root } from "./components/Root"; import { App } from "./components/App";
mount(Root, document.body); function setup() {
const app = new App();
app.mount(document.body);
}
// tests/components/Root.test.js ------------------------------------------------ utils.whenReady(setup);
import { Root } from "../../src/components/Root";
// tests/components/App.test.js ------------------------------------------------
import { App } from "../../src/components/App";
import { makeTestFixture, nextTick, click } from "../helpers"; import { makeTestFixture, nextTick, click } from "../helpers";
import { mount } from "@odoo/owl";
let fixture; let fixture;
@@ -254,9 +264,10 @@ afterEach(() => {
fixture.remove(); fixture.remove();
}); });
describe("Root", () => { describe("App", () => {
test("Works as expected...", async () => { test("Works as expected...", async () => {
await mount(Root, fixture); const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>"); expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
click(fixture, "div"); click(fixture, "div");
@@ -270,8 +281,9 @@ import { Component } from "@odoo/owl";
import "regenerator-runtime/runtime"; import "regenerator-runtime/runtime";
export async function nextTick() { export async function nextTick() {
await new Promise((resolve) => setTimeout(resolve)); return new Promise(function (resolve) {
await new Promise((resolve) => requestAnimationFrame(resolve)); setTimeout(() => Component.scheduler.requestAnimationFrame(() => resolve()));
});
} }
export function makeTestFixture() { export function makeTestFixture() {
+324 -291
View File
@@ -50,11 +50,10 @@ the following content:
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>OWL Todo App</title> <title>OWL Todo App</title>
<link rel="stylesheet" href="app.css" /> <link rel="stylesheet" href="app.css" />
</head>
<body>
<script src="owl.js"></script> <script src="owl.js"></script>
<script src="app.js"></script> <script src="app.js"></script>
</body> </head>
<body></body>
</html> </html>
``` ```
@@ -72,34 +71,45 @@ Note that we put everything inside an immediately executed function to avoid lea
anything to the global scope. anything to the global scope.
Finally, `owl.js` should be the last version downloaded from the Owl repository (you can use `owl.min.js` if you prefer). Be aware that you should download the `owl.iife.js` or `owl.iife.min.js`, because these files Finally, `owl.js` should be the last version downloaded from the Owl repository (you can use `owl.min.js` if you prefer). Be aware that you should download the `owl.iife.js` or `owl.iife.min.js`, because these files
are built to run directly on the browser, and rename it `owl.js` (other files such as `owl.cjs.js` are are built to run directly on the browser (other files such as `owl.cjs.js` are
built to be bundled by other tools). built to be bundled by other tools).
Now, the project should be ready. Loading the `index.html` file into a browser Now, the project should be ready. Loading the `index.html` file into a browser
should show an empty page, with the title `Owl Todo App`, and it should log a should show an empty page, with the title `Owl Todo App`, and it should log a
message such as `hello owl 2.x.y` in the console. message such as `hello owl 1.0.0` in the console.
## 2. Adding a first component ## 2. Adding a first component
An Owl application is made out of [components](../reference/component.md), with An Owl application is made out of [components](../reference/component.md), with
a single root component. Let us start by defining a `Root` component. Replace the a single root component. Let us start by defining an `App` component. Replace the
content of the function in `app.js` by the following code: content of the function in `app.js` by the following code:
```js ```js
const { Component, mount, xml } = owl; const { Component } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
// Owl Components // Owl Components
class Root extends Component { class App extends Component {
static template = xml`<div>todo app</div>`; static template = xml`<div>todo app</div>`;
} }
mount(Root, document.body); // Setup code
function setup() {
const app = new App();
app.mount(document.body);
}
whenReady(setup);
``` ```
Now, reloading the page in a browser should display a message. Now, reloading the page in a browser should display a message.
The code is pretty simple: we define a component with an inline template, then The code is pretty simple, but let us explain the last line in more detail. The
mount it in the document body. browser tries to execute the javascript code in `app.js` as quickly as possible,
and it could happen that the DOM is not ready yet when we try to mount the `App`
component. To avoid this situation, we use the [`whenReady`](../reference/utils.md#whenready)
helper to delay the execution of the `setup` function until the DOM is ready.
Note 1: in a larger project, we would split the code in multiple files, with Note 1: in a larger project, we would split the code in multiple files, with
components in a sub folder, and a main file that would initialize the application. components in a sub folder, and a main file that would initialize the application.
@@ -140,20 +150,20 @@ with the following keys:
tasks. Since the title is something created/edited by the user, it offers tasks. Since the title is something created/edited by the user, it offers
no guarantee that it is unique. So, we will generate a unique `id` number for no guarantee that it is unique. So, we will generate a unique `id` number for
each task. each task.
- `text`: a string, to explain what the task is about. - `title`: a string, to explain what the task is about.
- `isCompleted`: a boolean, to keep track of the status of the task - `isCompleted`: a boolean, to keep track of the status of the task
Now that we decided on the internal format of the state, let us add some demo Now that we decided on the internal format of the state, let us add some demo
data and a template to the `App` component: data and a template to the `App` component:
```js ```js
class Root extends Component { class App extends Component {
static template = xml/* xml */ ` static template = xml/* xml */ `
<div class="task-list"> <div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id"> <t t-foreach="tasks" t-as="task" t-key="task.id">
<div class="task"> <div class="task">
<input type="checkbox" t-att-checked="task.isCompleted"/> <input type="checkbox" t-att-checked="task.isCompleted"/>
<span><t t-esc="task.text"/></span> <span><t t-esc="task.title"/></span>
</div> </div>
</t> </t>
</div>`; </div>`;
@@ -161,12 +171,12 @@ class Root extends Component {
tasks = [ tasks = [
{ {
id: 1, id: 1,
text: "buy milk", title: "buy milk",
isCompleted: true, isCompleted: true,
}, },
{ {
id: 2, id: 2,
text: "clean house", title: "clean house",
isCompleted: false, isCompleted: false,
}, },
]; ];
@@ -236,25 +246,29 @@ a little bit:
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Task Component // Task Component
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
class Task extends Component { const TASK_TEMPLATE = xml /* xml */`
static template = xml /* xml */`
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''"> <div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted"/> <input type="checkbox" t-att-checked="props.task.isCompleted"/>
<span><t t-esc="props.task.title"/></span> <span><t t-esc="props.task.title"/></span>
</div>`; </div>`;
static props = ["task"];
class Task extends Component {
static template = TASK_TEMPLATE;
static props = ["task"];
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Root Component // App Component
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
class Root extends Component { const APP_TEMPLATE = xml /* xml */`
static template = xml /* xml */`
<div class="task-list"> <div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id"> <t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/> <Task task="task"/>
</t> </t>
</div>`; </div>`;
class App extends Component {
static template = APP_TEMPLATE;
static components = { Task }; static components = { Task };
tasks = [ tasks = [
@@ -263,9 +277,15 @@ class Root extends Component {
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Setup // Setup code
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
mount(Root, document.body, {dev: true}); function setup() {
owl.config.mode = "dev";
const app = new App();
app.mount(document.body);
}
whenReady(setup);
``` ```
A lot of stuff happened here: A lot of stuff happened here:
@@ -274,22 +294,24 @@ A lot of stuff happened here:
- whenever we define a sub component, it needs to be added to the static - whenever we define a sub component, it needs to be added to the static
[`components`](../reference/component.md#static-properties) [`components`](../reference/component.md#static-properties)
key of its parent, so Owl can get a reference to it, key of its parent, so Owl can get a reference to it,
- the templates have been extracted out of the components, to make it easier to
differentiate the "view/template" code from the "script/behavior" code,
- the `Task` component has a `props` key: this is only useful for validation - the `Task` component has a `props` key: this is only useful for validation
purpose. It says that each `Task` should be given exactly one prop, named purpose. It says that each `Task` should be given exactly one prop, named
`task`. If this is not the case, Owl will throw an `task`. If this is not the case, Owl will throw an
[error](../reference/props_validation.md). This is extremely [error](../reference/props_validation.md). This is extremely
useful when refactoring components useful when refactoring components
- finally, to activate the props validation, we need to set Owl's - finally, to activate the props validation, we need to set Owl's
[mode](../reference/config.md#mode) to `dev`. This is done in the last argument [mode](../reference/config.md#mode) to `dev`. This is done in the `setup`
of the `mount` function. Note that this should be removed when an app is used in a real function. Note that this should be removed when an app is used in a real
production environment, since `dev` mode is slightly slower, due to extra production environment, since `dev` mode is slightly slower, due to extra
checks and validations. checks and validations.
## 6. Adding tasks (part 1) ## 6. Adding tasks (part 1)
We still use a list of hardcoded tasks. It's really time to give the user a way We still use a list of hardcoded tasks. It's really time to give the user a way
to add tasks himself. The first step is to add an input to the `Root` component. to add tasks himself. The first step is to add an input to the `App` component.
But this input will be outside of the task list, so we need to adapt `Root` But this input will be outside of the task list, so we need to adapt `App`
template, js, and css: template, js, and css:
```xml ```xml
@@ -307,9 +329,9 @@ template, js, and css:
addTask(ev) { addTask(ev) {
// 13 is keycode for ENTER // 13 is keycode for ENTER
if (ev.keyCode === 13) { if (ev.keyCode === 13) {
const text = ev.target.value.trim(); const title = ev.target.value.trim();
ev.target.value = ""; ev.target.value = "";
console.log('adding task', text); console.log('adding task', title);
// todo // todo
} }
} }
@@ -338,9 +360,10 @@ task. Notice that when you load the page, the input is not focused. But adding
tasks is a core feature of a task list, so let us make it as fast as possible by tasks is a core feature of a task list, so let us make it as fast as possible by
focusing the input. focusing the input.
We need to execute code when the `Root` component is ready (mounted). Let's do Since `App` is a component, it has a
that using the `onMounted` hook. We will also need to get a reference to the [`mounted` lifecycle method](../reference/component.md#lifecycle) that we can
input, by using the `t-ref` directive with the [`useRef`](../reference/hooks.md#useref) hook: implement. We will also need to get a reference to the input, by using the
`t-ref` directive with the [`useRef`](../reference/hooks.md#useref) hook:
```xml ```xml
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/> <input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
@@ -348,21 +371,22 @@ input, by using the `t-ref` directive with the [`useRef`](../reference/hooks.md#
```js ```js
// on top of file: // on top of file:
const { Component, mount, xml, useRef, onMounted } = owl; const { useRef } = owl.hooks;
``` ```
```js ```js
// in App // in App
setup() { inputRef = useRef("add-input");
const inputRef = useRef("add-input");
onMounted(() => inputRef.el.focus()); mounted() {
this.inputRef.el.focus();
} }
``` ```
This is a very common situation: whenever we need to perform some actions depending The `inputRef` is defined as a class field, so it is equivalent to defining it
on the lifecycle of a component, we need to do it in the `setup` method, by using in the constructor. It simply instructs Owl to keep a reference to anything with
one of the lifecycle hook. Here, we first get a reference to the `inputRef`, the corresponding `t-ref` keyword. We then implement the `mounted` lifecycle
then in the `onMounted` hook, we simply focus the html element. method, where we now have an active reference that we can use to focus the input.
## 7. Adding tasks (part 2) ## 7. Adding tasks (part 2)
@@ -383,12 +407,12 @@ Now, the `addTask` method can be implemented:
addTask(ev) { addTask(ev) {
// 13 is keycode for ENTER // 13 is keycode for ENTER
if (ev.keyCode === 13) { if (ev.keyCode === 13) {
const text = ev.target.value.trim(); const title = ev.target.value.trim();
ev.target.value = ""; ev.target.value = "";
if (text) { if (title) {
const newTask = { const newTask = {
id: this.nextId++, id: this.nextId++,
text: text, title: title,
isCompleted: false, isCompleted: false,
}; };
this.tasks.push(newTask); this.tasks.push(newTask);
@@ -406,7 +430,7 @@ the user interface. We can fix the issue by making `tasks` reactive, with the
```js ```js
// on top of the file // on top of the file
const { Component, mount, xml, useRef, onMounted, useState } = owl; const { useRef, useState } = owl.hooks;
// replace the task definition in App with the following: // replace the task definition in App with the following:
tasks = useState([]); tasks = useState([]);
@@ -421,8 +445,12 @@ did not change in opacity. This is because there is no code to modify the
`isCompleted` flag. `isCompleted` flag.
Now, this is an interesting situation: the task is displayed by the `Task` Now, this is an interesting situation: the task is displayed by the `Task`
component, but it is not the owner of its state, so ideally, it should not modify it. component, but it is not the owner of its state, so it cannot modify it. Instead,
However, for now, that's what we will do (this will be improved in a later step). we want to communicate the request to toggle a task to the `App` component.
Since `App` is a parent of `Task`, we can
[trigger](../reference/event_handling.md) an event in `Task` and listen
for it in `App`.
In `Task`, change the `input` to: In `Task`, change the `input` to:
```xml ```xml
@@ -433,23 +461,36 @@ and add the `toggleTask` method:
```js ```js
toggleTask() { toggleTask() {
this.props.task.isCompleted = !this.props.task.isCompleted; this.trigger('toggle-task', {id: this.props.task.id});
}
```
We now need to listen for that event in the `App` template:
```xml
<div class="task-list" t-on-toggle-task="toggleTask">
```
and implement the `toggleTask` code:
```js
toggleTask(ev) {
const task = this.tasks.find(t => t.id === ev.detail.id);
task.isCompleted = !task.isCompleted;
} }
``` ```
## 9. Deleting tasks ## 9. Deleting tasks
Let us now add the possibility do delete tasks. This is different from the previous Let us now add the possibility do delete tasks. To do that, we first need to add
feature: deleting task has to be done on the task itself, but the actual operation a trash icon on each task, then we will proceed just like in the previous section.
need to be done on the task list. So, we need to communicate the request to the
`Root` component. This is usually done by providing a callback in a prop.
First, let us update the `Task` template, css and js: First, let us update the `Task` template, css and js:
```xml ```xml
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''"> <div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/> <input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
<span><t t-esc="props.task.text"/></span> <span><t t-esc="props.task.title"/></span>
<span class="delete" t-on-click="deleteTask">🗑</span> <span class="delete" t-on-click="deleteTask">🗑</span>
</div> </div>
``` ```
@@ -478,226 +519,220 @@ First, let us update the `Task` template, css and js:
``` ```
```js ```js
static props = ["task", "onDelete"];
deleteTask() { deleteTask() {
this.props.onDelete(this.props.task); this.trigger('delete-task', {id: this.props.task.id});
} }
``` ```
And now, we need to provide the `onDelete` callback to each tasks in the `Root` And now, we need to listen to the `delete-task` event in `App`:
component:
```xml ```xml
<Task task="task" onDelete.bind="deleteTask"/> <div class="task-list" t-on-toggle-task="toggleTask" t-on-delete-task="deleteTask">
``` ```
```js ```js
deleteTask(task) { deleteTask(ev) {
const index = this.tasks.findIndex(t => t.id === task.id); const index = this.tasks.findIndex(t => t.id === ev.detail.id);
this.tasks.splice(index, 1); this.tasks.splice(index, 1);
} }
``` ```
Notice that the `onDelete` prop is defined with a `.bind` suffix: this is a special
suffix that makes sure the function callback is bound to the component.
## 10. Using a store ## 10. Using a store
Looking at the code, it is apparent that all the code handling tasks is scattered Looking at the code, it is apparent that we now have code to handle tasks
all around the application. Also, it mixes UI code and business logic scattered in more than one place. Also, it mixes UI code and business logic
code. Owl does not provide any high level abstraction to manage business logic, code. Owl has a way to manage state separately from the user interface: a
but it is easy to do it with the basic reactivity primitives (`useState` and `reactive`). [`Store`](../reference/store.md).
Let us use it in our application to implement a central store. This is a pretty Let us use it in our application. This is a pretty large refactoring (for our
large refactoring (for our application), since it involves extracting all task application), since it involves extracting all task related code out of the
related code out of the components. Here is the new content of the `app.js` file: components. Here is the new content of the `app.js` file:
```js ```js
const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl; const { Component, Store } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
const { useRef, useDispatch, useStore } = owl.hooks;
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Store // Store
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
function useStore() { const actions = {
const env = useEnv(); addTask({ state }, title) {
return useState(env.store); title = title.trim();
} if (title) {
// -------------------------------------------------------------------------
// TaskList
// -------------------------------------------------------------------------
class TaskList {
nextId = 1;
tasks = [];
addTask(text) {
text = text.trim();
if (text) {
const task = { const task = {
id: this.nextId++, id: state.nextId++,
text: text, title: title,
isCompleted: false, isCompleted: false,
}; };
this.tasks.push(task); state.tasks.push(task);
} }
} },
toggleTask({ state }, id) {
toggleTask(task) { const task = state.tasks.find((t) => t.id === id);
task.isCompleted = !task.isCompleted; task.isCompleted = !task.isCompleted;
} },
deleteTask({ state }, id) {
deleteTask(task) { const index = state.tasks.findIndex((t) => t.id === id);
const index = this.tasks.findIndex((t) => t.id === task.id); state.tasks.splice(index, 1);
this.tasks.splice(index, 1); },
} };
} const initialState = {
nextId: 1,
function createTaskStore() { tasks: [],
return reactive(new TaskList()); };
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Task Component // Task Component
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
class Task extends Component { const TASK_TEMPLATE = xml/* xml */ `
static template = xml/* xml */ `
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''"> <div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="() => store.toggleTask(props.task)"/> <input type="checkbox" t-att-checked="props.task.isCompleted"
<span><t t-esc="props.task.text"/></span> t-on-click="dispatch('toggleTask', props.task.id)"/>
<span class="delete" t-on-click="() => store.deleteTask(props.task)">🗑</span> <span><t t-esc="props.task.title"/></span>
<span class="delete" t-on-click="dispatch('deleteTask', props.task.id)">🗑</span>
</div>`; </div>`;
class Task extends Component {
static template = TASK_TEMPLATE;
static props = ["task"]; static props = ["task"];
dispatch = useDispatch();
setup() {
this.store = useStore();
}
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Root Component // App Component
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
class Root extends Component { const APP_TEMPLATE = xml/* xml */ `
static template = xml/* xml */ `
<div class="todo-app"> <div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/> <input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list"> <div class="task-list">
<t t-foreach="store.tasks" t-as="task" t-key="task.id"> <t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/> <Task task="task"/>
</t> </t>
</div> </div>
</div>`; </div>`;
class App extends Component {
static template = APP_TEMPLATE;
static components = { Task }; static components = { Task };
setup() { inputRef = useRef("add-input");
const inputRef = useRef("add-input"); tasks = useStore((state) => state.tasks);
onMounted(() => inputRef.el.focus()); dispatch = useDispatch();
this.store = useStore();
mounted() {
this.inputRef.el.focus();
} }
addTask(ev) { addTask(ev) {
// 13 is keycode for ENTER // 13 is keycode for ENTER
if (ev.keyCode === 13) { if (ev.keyCode === 13) {
this.store.addTask(ev.target.value); this.dispatch("addTask", ev.target.value);
ev.target.value = ""; ev.target.value = "";
} }
} }
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Setup // Setup code
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
const env = { function setup() {
store: createTaskStore(), owl.config.mode = "dev";
}; const store = new Store({ actions, state: initialState });
mount(Root, document.body, { dev: true, env }); App.env.store = store;
const app = new App();
app.mount(document.body);
}
whenReady(setup);
``` ```
## 11. Saving tasks in local storage ## 11-Saving tasks in local storage
Now, our TodoApp works great, except if the user closes or refresh the browser! Now, our TodoApp works great, except if the user closes or refresh the browser!
It is really inconvenient to only keep the state of the application in memory. It is really inconvenient to only keep the state of the application in memory.
To fix this, we will save the tasks in the local storage. With our current To fix this, we will save the tasks in the local storage. With our current
codebase, it is a simple change: we need to save tasks to local storage and codebase, it is a simple change: only the setup code needs to be updated.
listen to any change.
```js ```js
class TaskList { function makeStore() {
constructor(tasks) { const localState = window.localStorage.getItem("todoapp");
this.tasks = tasks || []; const state = localState ? JSON.parse(localState) : initialState;
const taskIds = this.tasks.map((t) => t.id); const store = new Store({ state, actions });
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1; store.on("update", null, () => {
} localStorage.setItem("todoapp", JSON.stringify(store.state));
// ... });
return store;
} }
function createTaskStore() { function setup() {
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks)); owl.config.mode = "dev";
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]"); App.env.store = makeStore();
const taskStore = reactive(new TaskList(initialTasks), saveTasks); const app = new App();
saveTasks(); app.mount(document.body);
return taskStore;
} }
``` ```
The key point is that the `reactive` function takes a callback that will be called The key point is to use the fact that the store is an
every time an observed value is changed. Note that we need to call the `saveTasks` [`EventBus`](../reference/event_bus.md) which triggers an `update` event
method initially to make sure we observe all current values. whenever it is updated.
## 12. Filtering tasks ## 12. Filtering tasks
We are almost done, we can add/update/delete tasks. The only missing feature is We are almost done, we can add/update/delete tasks. The only missing feature is
the possibility to display the task according to their completed status. We will the possibility to display the task according to their completed status. We will
need to keep track of the state of the filter in `Root`, then filter the visible need to keep track of the state of the filter in `App`, then filter the visible
tasks according to its value. tasks according to its value.
```js ```js
class Root extends Component { // on top of file, readd useState:
static template = xml /* xml */` const { useRef, useDispatch, useState, useStore } = owl.hooks;
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/> // in App:
<div class="task-list"> filter = useState({value: "all"})
get displayedTasks() {
switch (this.filter.value) {
case "active": return this.tasks.filter(t => !t.isCompleted);
case "completed": return this.tasks.filter(t => t.isCompleted);
case "all": return this.tasks;
}
}
setFilter(filter) {
this.filter.value = filter;
}
```
Finally, we need to display the visible filters. We can do that, and at the
same time, display the number of tasks in a small panel below the main list:
```xml
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list">
<t t-foreach="displayedTasks" t-as="task" t-key="task.id"> <t t-foreach="displayedTasks" t-as="task" t-key="task.id">
<Task task="task"/> <Task task="task"/>
</t> </t>
</div> </div>
<div class="task-panel" t-if="store.tasks.length"> <div class="task-panel" t-if="tasks.length">
<div class="task-counter"> <div class="task-counter">
<t t-esc="displayedTasks.length"/> <t t-esc="displayedTasks.length"/>
<t t-if="displayedTasks.length lt store.tasks.length"> <t t-if="displayedTasks.length lt tasks.length">
/ <t t-esc="store.tasks.length"/> / <t t-esc="tasks.length"/>
</t> </t>
task(s) task(s)
</div> </div>
<div> <div>
<span t-foreach="['all', 'active', 'completed']" <span t-foreach="['all', 'active', 'completed']"
t-as="f" t-key="f" t-as="f" t-key="f"
t-att-class="{active: filter.value===f}" t-att-class="{active: filter.value===f}"
t-on-click="() => this.setFilter(f)" t-on-click="setFilter(f)"
t-esc="f"/> t-esc="f"/>
</div> </div>
</div> </div>
</div>`; </div>
setup() {
...
this.filter = useState({ value: "all" });
}
get displayedTasks() {
const tasks = this.store.tasks;
switch (this.filter.value) {
case "active": return tasks.filter(t => !t.isCompleted);
case "completed": return tasks.filter(t => t.isCompleted);
case "all": return tasks;
}
}
setFilter(filter) {
this.filter.value = filter;
}
}
``` ```
```css ```css
@@ -722,8 +757,8 @@ class Root extends Component {
} }
``` ```
Notice here that we set dynamically the css class of the filter with the object Notice here that we set dynamically the class of the filter with the object
syntax. syntax: each key is a class that we want to set if its value is truthy.
## 13. The Final Touch ## 13. The Final Touch
@@ -738,16 +773,16 @@ the user experience.
} }
``` ```
2. Make the text of a task clickable, to toggle its checkbox: 2. Make the title of a task clickable, to toggle its checkbox:
```xml ```xml
<input type="checkbox" t-att-checked="props.task.isCompleted" <input type="checkbox" t-att-checked="props.task.isCompleted"
t-att-id="props.task.id" t-att-id="props.task.id"
t-on-click="dispatch('toggleTask', props.task.id)"/> t-on-click="dispatch('toggleTask', props.task.id)"/>
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label> <label t-att-for="props.task.id"><t t-esc="props.task.title"/></label>
``` ```
3. Strike the text of completed task: 3. Strike the title of completed task:
```css ```css
.task.done label { .task.done label {
@@ -779,145 +814,143 @@ For reference, here is the final code:
```js ```js
(function () { (function () {
const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl; const { Component, Store } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
const { useRef, useDispatch, useState, useStore } = owl.hooks;
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Store // Store
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
function useStore() { const actions = {
const env = useEnv(); addTask({ state }, title) {
return useState(env.store); title = title.trim();
} if (title) {
// -------------------------------------------------------------------------
// TaskList
// -------------------------------------------------------------------------
class TaskList {
constructor(tasks) {
this.tasks = tasks || [];
const taskIds = this.tasks.map((t) => t.id);
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
}
addTask(text) {
text = text.trim();
if (text) {
const task = { const task = {
id: this.nextId++, id: state.nextId++,
text: text, title: title,
isCompleted: false, isCompleted: false,
}; };
this.tasks.push(task); state.tasks.push(task);
} }
} },
toggleTask({ state }, id) {
toggleTask(task) { const task = state.tasks.find((t) => t.id === id);
task.isCompleted = !task.isCompleted; task.isCompleted = !task.isCompleted;
} },
deleteTask({ state }, id) {
const index = state.tasks.findIndex((t) => t.id === id);
state.tasks.splice(index, 1);
},
};
deleteTask(task) { const initialState = {
const index = this.tasks.findIndex((t) => t.id === task.id); nextId: 1,
this.tasks.splice(index, 1); tasks: [],
} };
}
function createTaskStore() {
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
saveTasks();
return taskStore;
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Task Component // Task Component
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
const TASK_TEMPLATE = xml/* xml */ `
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted"
t-att-id="props.task.id"
t-on-click="dispatch('toggleTask', props.task.id)"/>
<label t-att-for="props.task.id"><t t-esc="props.task.title"/></label>
<span class="delete" t-on-click="dispatch('deleteTask', props.task.id)">🗑</span>
</div>`;
class Task extends Component { class Task extends Component {
static template = xml/* xml */ ` static template = TASK_TEMPLATE;
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox"
t-att-id="props.task.id"
t-att-checked="props.task.isCompleted"
t-on-click="() => store.toggleTask(props.task)"/>
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
<span class="delete" t-on-click="() => store.deleteTask(props.task)">🗑</span>
</div>`;
static props = ["task"]; static props = ["task"];
dispatch = useDispatch();
setup() {
this.store = useStore();
}
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Root Component // App Component
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
class Root extends Component { const APP_TEMPLATE = xml/* xml */ `
static template = xml/* xml */ ` <div class="todo-app">
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/> <input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list"> <div class="task-list">
<t t-foreach="displayedTasks" t-as="task" t-key="task.id"> <Task t-foreach="displayedTasks" t-as="task" t-key="task.id" task="task"/>
<Task task="task"/>
</t>
</div> </div>
<div class="task-panel" t-if="store.tasks.length"> <div class="task-panel" t-if="tasks.length">
<div class="task-counter"> <div class="task-counter">
<t t-esc="displayedTasks.length"/> <t t-esc="displayedTasks.length"/>
<t t-if="displayedTasks.length lt store.tasks.length"> <t t-if="displayedTasks.length lt tasks.length">
/ <t t-esc="store.tasks.length"/> / <t t-esc="tasks.length"/>
</t> </t>
task(s) task(s)
</div> </div>
<div> <div>
<span t-foreach="['all', 'active', 'completed']" <span t-foreach="['all', 'active', 'completed']"
t-as="f" t-key="f" t-as="f" t-key="f"
t-att-class="{active: filter.value===f}" t-att-class="{active: filter.value===f}"
t-on-click="() => this.setFilter(f)" t-on-click="setFilter(f)"
t-esc="f"/> t-esc="f"/>
</div> </div>
</div> </div>
</div>`; </div>`;
class App extends Component {
static template = APP_TEMPLATE;
static components = { Task }; static components = { Task };
setup() { inputRef = useRef("add-input");
const inputRef = useRef("add-input"); tasks = useStore((state) => state.tasks);
onMounted(() => inputRef.el.focus()); filter = useState({ value: "all" });
this.store = useStore(); dispatch = useDispatch();
this.filter = useState({ value: "all" });
mounted() {
this.inputRef.el.focus();
} }
addTask(ev) { addTask(ev) {
// 13 is keycode for ENTER // 13 is keycode for ENTER
if (ev.keyCode === 13) { if (ev.keyCode === 13) {
this.store.addTask(ev.target.value); this.dispatch("addTask", ev.target.value);
ev.target.value = ""; ev.target.value = "";
} }
} }
get displayedTasks() { get displayedTasks() {
const tasks = this.store.tasks;
switch (this.filter.value) { switch (this.filter.value) {
case "active": case "active":
return tasks.filter((t) => !t.isCompleted); return this.tasks.filter((t) => !t.isCompleted);
case "completed": case "completed":
return tasks.filter((t) => t.isCompleted); return this.tasks.filter((t) => t.isCompleted);
case "all": case "all":
return tasks; return this.tasks;
} }
} }
setFilter(filter) { setFilter(filter) {
this.filter.value = filter; this.filter.value = filter;
} }
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Setup // Setup code
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
const env = { store: createTaskStore() }; function makeStore() {
mount(Root, document.body, { dev: true, env }); const localState = window.localStorage.getItem("todoapp");
const state = localState ? JSON.parse(localState) : initialState;
const store = new Store({ state, actions });
store.on("update", null, () => {
localStorage.setItem("todoapp", JSON.stringify(store.state));
});
return store;
}
function setup() {
owl.config.mode = "dev";
App.env.store = makeStore();
const app = new App();
app.mount(document.body);
}
whenReady(setup);
})(); })();
``` ```
+95 -5
View File
@@ -4,7 +4,7 @@ OWL, React and Vue have the same main feature: they allow developers to build
declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences. declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences.
In this page, we try to highlight some of these differences. Obviously, a lot of In this page, we try to highlight some of these differences. Obviously, a lot of
effort was put to be fair. However, if you disagree with some of the points effort was done to be fair. However, if you disagree with some of the points
discussed, feel free to open an issue/submit a PR to correct this text. discussed, feel free to open an issue/submit a PR to correct this text.
## Content ## Content
@@ -14,7 +14,8 @@ discussed, feel free to open an issue/submit a PR to correct this text.
- [Tooling/Build Step](#toolingbuild-step) - [Tooling/Build Step](#toolingbuild-step)
- [Templating](#templating) - [Templating](#templating)
- [Asynchronous rendering](#asynchronous-rendering) - [Asynchronous rendering](#asynchronous-rendering)
- [Reactivity](#reactivity) - [Reactiveness](#reactiveness)
- [State Management](#state-management)
- [Hooks](#hooks) - [Hooks](#hooks)
## Size ## Size
@@ -46,7 +47,7 @@ components are fast enough for all our usecases, and making it as simple as
possible for developers is more valuable (for us). possible for developers is more valuable (for us).
Also, functions or class based components are more than just syntax. Functions Also, functions or class based components are more than just syntax. Functions
come with a mindset of composition and class are about inheritance. Clearly, comes with a mindset of composition and class are about inheritance. Clearly,
both of these are important mechanisms for reusing code. Also, one does not both of these are important mechanisms for reusing code. Also, one does not
exclude the other. exclude the other.
@@ -172,7 +173,7 @@ more convoluted. For example, in Vue, you need to use a dynamic import keyword
that needs to be transpiled at build time in order for the component to be loaded that needs to be transpiled at build time in order for the component to be loaded
asynchronously (see [the documentation](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components)). asynchronously (see [the documentation](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components)).
## Reactivity ## Reactiveness
React has a simple model: whenever the state changes, it is React has a simple model: whenever the state changes, it is
replaced with a new state (via the `setState` method). Then, the DOM is patched. replaced with a new state (via the `setState` method). Then, the DOM is patched.
@@ -188,6 +189,95 @@ with a `Proxy`, which means that it is totally transparent to the developers.
Adding new keys is supported. Once any part of the state has been changed, a Adding new keys is supported. Once any part of the state has been changed, a
rendering is scheduled in the next microtask tick (promise queue). rendering is scheduled in the next microtask tick (promise queue).
## State Management
Managing the state of an application is a tricky issue. Many solutions have
been proposed these last few years. It also depends on the kind of application we
are talking about. A small application may not need much more than a simple
object to contain its state.
However, there are some common solutions for React and Vue: redux and vuex.
Both of them are a centralized store that own the state, and they dictate how
the state can be mutated.
**Redux**
In Redux, the state is mutated by reducers. Reducers are functions
that modify the state by returning a different object:
```javascript
...
switch (action.type) {
case ADD_TODO: {
const { id, content } = action.payload;
return {
...state,
allIds: [...state.allIds, id],
byIds: {
...state.byIds,
[id]: {
content,
completed: false
}
}
};
}
```
This is a little bit awkward to write, but this allows the component system to
check if a part of the state was changed. This is exactly what is done by the
`connect` function: it create a _connected_ component, which is subscribed to
the state and triggers a rerender if some part of the state was modified.
**VueX**
VueX is based on a different principle: the state is mutated through
some special functions (the mutations), which modify the state in place:
```javascript
function ({state}, payload) {
const { id, content } = payload;
const message = {id, content, completed: false};
state.messages.push(message)
}
```
This is simpler, but there is a little bit more happening behind the scene:
each key from the state is silently replaced by getters and setters, and VueX
keeps track of who get data, and retrigger a render when it was changed.
**Owl**
Owl store is a little bit like a mix of redux and vuex: it has actions (but not
mutations), and like VueX, it keeps track of the state changes. However, it does
not notify a component when the state changes. Instead, components need to connect
to the store like in redux, with the `useStore` hook (see the [store documentation](../reference/store.md#connecting-a-component)).
```javascript
const actions = {
increment({ state }, val) {
state.counter.value += val;
},
};
const state = {
counter: { value: 0 },
};
const store = new owl.Store({ state, actions });
class Counter extends Component {
static template = xml`
<button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="counter.value"/>]
</button>`;
counter = useStore((state) => state.counter);
dispatch = useDispatch();
}
Counter.env.store = store;
const counter = new Counter();
```
## Hooks ## Hooks
[Hooks](https://reactjs.org/docs/hooks-intro.html#motivation) recently took over [Hooks](https://reactjs.org/docs/hooks-intro.html#motivation) recently took over
@@ -252,4 +342,4 @@ class Example extends Component {
Since the Owl framework had hooks from early in its life, its main APIs Since the Owl framework had hooks from early in its life, its main APIs
are designed to be interacted with hooks from the start. For example, the are designed to be interacted with hooks from the start. For example, the
`Context` abstraction. `Context` and `Store` abstractions.
+12 -12
View File
@@ -61,14 +61,14 @@ because a lot of the state is hidden in their internals.
React or Vue have a huge community, and a lot of effort have been made into their React or Vue have a huge community, and a lot of effort have been made into their
tooling. This is wonderful, but at the same time, a pretty big issue for Odoo: tooling. This is wonderful, but at the same time, a pretty big issue for Odoo:
since the assets are totally dynamic (and could change whenever the user installs since the assets are totally dynamic (and could change whenever the user install
or removes an addon), we need to have all that kind of tooling on the production or remove an addon), we need to have all that kind of tooling on the production
servers. This is certainly not ideal. servers. This is certainly not ideal.
Also, this makes it very complicated to setup Vue or React tools: Odoo code is Also, this makes it very complicated to setup Vue or React tools: Odoo code is
not a simple file that import other files. It changes all the time, assets not a simple file that import other files. It changes all the time, assets
are bundled differently in different contexts. This is the reason why Odoo has are bundled differently in different contexts. This is the reason why Odoo has
its own module system, which are resolved at runtime, by the browser. The its own module system, which are resolve at runtime, by the browser. The
dynamic nature of Odoo means that we often need to delay work as late as possible dynamic nature of Odoo means that we often need to delay work as late as possible
(in other word, we want a JIT user interface!) (in other word, we want a JIT user interface!)
@@ -78,12 +78,12 @@ deploy. Using React without JSX, or Vue without vue file is not very appealing.
At the same time, Owl is designed to solve this issue: it compiles templates At the same time, Owl is designed to solve this issue: it compiles templates
by the browser, it doesn't need much code for that, since we use the XML parser by the browser, it doesn't need much code for that, since we use the XML parser
built into each browser. Owl works with or without any additional tooling. It built into each browser. Owl works with or without any additional tooling. It
can use template strings to write single file components, and is easy to integrate can use template strings to write single file component, and is easy to integrate
in any html page, with a simple `<script>` tag. in any html page, with a simple `<script>` tag.
## Template based ## Template based
Odoo stores templates as XML documents in a database. This is very powerful, since Odoo stores template as XML document in a database. This is very powerful, since
this allow the use of xpaths to customize other templates. This is a very this allow the use of xpaths to customize other templates. This is a very
important feature of odoo, and one of the key to Odoo modularity. important feature of odoo, and one of the key to Odoo modularity.
@@ -104,12 +104,12 @@ awkward, and very confusing.
## Developer Experience ## Developer Experience
This brings us to the following point: developer experience. We see this choice This brings us to the following point: developer experience. We see this choice
as an investment for the future, and we want to make onboarding developers as as an investment for the future, and we want to make onboarding developer as
easy as possible. easy as possible.
While many javascript professionals clearly think that react/vue is not difficult While many javascript professionals clearly think that react/vue is not difficult
(which is true to some extent), it is alsy true that many non js specialists are (which is true to some extent), it is alsy true that many non js specialists are
overwhelmed with the frontend world: functional components, hooks, and many other overwhelmed with the frontend world: functional component, hooks, and many other
fancy words. Also, what is available in the compilation context may be difficult, fancy words. Also, what is available in the compilation context may be difficult,
there is a lot of black magic going on in pretty much every framework. Vue there is a lot of black magic going on in pretty much every framework. Vue
somehow join various namespaces into one, under the hood, and add various internal somehow join various namespaces into one, under the hood, and add various internal
@@ -135,7 +135,7 @@ needs: Odoo will fetch templates from the database and need to compile them only
at the last possible moment, so we can apply all necessary xpaths. at the last possible moment, so we can apply all necessary xpaths.
Even more: Odoo needs to be able to generate (and compile) templates at runtime. Even more: Odoo needs to be able to generate (and compile) templates at runtime.
Currently, Odoo form views interpret an xml description. But the form view code Currently, Odoo form views interpret a xml description. But the form view code
then needs to do a lot of complicated operations. With Owl, we will be able to then needs to do a lot of complicated operations. With Owl, we will be able to
transform a view description into a QWeb template, then compile that and use it transform a view description into a QWeb template, then compile that and use it
immediately. immediately.
@@ -147,16 +147,16 @@ For example, the reactivity system. We like the way Vue did it, but it has a
flaw: it is not really optional. There is actually a way to opt out of the reactivity flaw: it is not really optional. There is actually a way to opt out of the reactivity
system by freezing the state, but then, it is freezed. system by freezing the state, but then, it is freezed.
And there certainly are situations where we need a state, which is not read-only, And there certainly are situations where we need a state, which is not readonly,
and not observed. For example, imagine a spreadsheet component. It may have a and not observed. For example, imagine a spreadsheet component. It may have a
very large internal state, and it knows exactly when it needs to be rendered very large internal state, and it knows exactly when it needs to be rendered
(basically, whenever the user performs some action). Then, observing its state (basically, whenever the user perform some action). Then, observing its state
is a net performance loss, both for the CPU and the memory. is a net performance loss, both for the CPU and the memory.
## Concurrency ## Concurrency
Many applications are happy to simply display a spinner whenever a new asynchronous Many applications are happy to simply display a spinner whenever a new asynchronous
action is performed, but Odoo wants a different user experience: most asynchronous action is performed, but Odoo want a different user experience: most asynchronous
state changes are not displayed until ready. This is sometimes called a concurrent state changes are not displayed until ready. This is sometimes called a concurrent
mode: the UI is rendered in memory, and displayed only when it is ready (and mode: the UI is rendered in memory, and displayed only when it is ready (and
only if it has not been cancelled by subsequent user actions). only if it has not been cancelled by subsequent user actions).
@@ -175,6 +175,6 @@ that current standard frameworks are not tailored to our needs. It is perfectly
fine, because they each chose a different set of tradeoffs. fine, because they each chose a different set of tradeoffs.
However, we feel that there is still room in the framework world for something However, we feel that there is still room in the framework world for something
that is different. For a framework that makes choices compatible with Odoo. that is different. For a framework that make choices compatible with Odoo.
And that is why we built Owl 🦉. And that is why we built Owl 🦉.
+55
View File
@@ -0,0 +1,55 @@
# 🦉 OWL Documentation 🦉
## Learning Owl
Are you new to Owl? This is the place to start!
- [Tutorial: create a TodoList application](learning/tutorial_todoapp.md)
- [Quick Overview](learning/overview.md)
- [How to start an Owl project](learning/quick_start.md)
- [How to test Components](learning/how_to_test.md)
- [How to write Single File Components](learning/how_to_write_sfc.md)
- [How to write debug Owl applications](learning/how_to_debug.md)
## Reference
You will find here a complete reference of every feature, class or object
provided by Owl.
- [Animations](reference/animations.md)
- [Component](reference/component.md)
- [Content](reference/content.md)
- [Concurrency Model](reference/concurrency_model.md)
- [Configuration](reference/config.md)
- [Context](reference/context.md)
- [Environment](reference/environment.md)
- [Event Bus](reference/event_bus.md)
- [Event Handling](reference/event_handling.md)
- [Error Handling](reference/error_handling.md)
- [Hooks](reference/hooks.md)
- [Miscellaneous Components](reference/misc.md)
- [Observer](reference/observer.md)
- [Props](reference/props.md)
- [Props Validation](reference/props_validation.md)
- [QWeb Templating Language](reference/qweb_templating_language.md)
- [QWeb Engine](reference/qweb_engine.md)
- [Router](reference/router.md)
- [Store](reference/store.md)
- [Slots](reference/slots.md)
- [Tags](reference/tags.md)
- [Utils](reference/utils.md)
## Other Topics
This section provides miscellaneous document that explains some topics
which cannot be considered either a tutorial, or reference documentation.
- [Owl architecture: the Virtual DOM](miscellaneous/vdom.md)
- [Owl architecture: the rendering pipeline](miscellaneous/rendering.md)
- [Comparison with React/Vue](miscellaneous/comparison.md)
- [Why did Odoo built Owl?](miscellaneous/why_owl.md)
---
Found an issue in the documentation? A broken link? Some outdated information?
Please open an issue or submit a PR!
+9 -41
View File
@@ -52,20 +52,21 @@ sequence of events will happen:
At node insertion: At node insertion:
- the css classes `name-enter` and `name-enter-active` will be added directly - the css classes `name-enter` and `name-enter-active` will be added directly
when the node is inserted into the DOM. when the node is inserted into the DOM,
- on the next animation frame: the css class `name-enter` will be removed and the - on the next animation frame: the css class `name-enter` will be removed and the
class `name-enter-to` will be added (so they can be used to trigger css class `name-enter-to` will be added (so they can be used to trigger css
transition effects). transition effects),
- at the end of the transition, `name-enter-to` and `name-enter-active` will be removed. - the css class `name-enter-active` will be removed whenever a css transition
ends.
At node destruction: At node destruction:
- the css classes `name-leave` and `name-leave-active` will be added before the - the css classes `name-leave` and `name-leave-active` will be added before the
node is removed to the DOM. node is removed to the DOM,
- on the next animation frame: the css class `name-leave` will be removed and the - the css class `name-leave` will be removed on the next animation frame (so it
class `name-leave-to` will be added (so they can be used to trigger css can be used to trigger css transition effects),
transition effects). - the css class `name-leave-active` will be removed whenever a css transition
- at the end of the transition, `name-leave-to` and `name-leave-active` will be removed. ends. Only then will the element be removed from the DOM.
For example, a simple fade in/out effect can be done with this: For example, a simple fade in/out effect can be done with this:
@@ -92,36 +93,3 @@ Notes:
Owl does not support more than one transition on a single node, so the Owl does not support more than one transition on a single node, so the
`t-transition` expression must be a single value (i.e. no space allowed). `t-transition` expression must be a single value (i.e. no space allowed).
## SCSS Mixins
If you use SCSS, you can use mixins to make generic animations. Here is an exemple with a fade in / fade out animation:
```scss
@mixin animation-fade($time, $name) {
.#{$name}_fade-enter-active,
.#{$name}_fade-active {
transition: all $time;
}
.#{$name}_fade-enter {
opacity: 0;
}
.#{$name}_fade-leave-to {
opacity: 0;
}
}
```
Usage:
```scss
@include animation-fade(0.5s, "o_notification");
```
You can now have in your template:
```xml
<SomeTag t-transition="o_notification_fade"/>
```
-33
View File
@@ -1,33 +0,0 @@
# 🦉 Browser 🦉
## Content
- [Overview](#overview)
- [Browser Content](#browser-content)
## Overview
The browser object contains some browser native APIs, such as `setTimeout`, that
are used by Owl and its utility functions. They are exposed with the intent of
making them mockable if necessary.
```js
owl.browser.setTimeout === window.setTimeout; // return true
```
For now, this object contains some functions that are not used by Owl. They
will eventually be removed in Owl 2.0.
## Browser Content
More specifically, the `browser` object contains the following methods and objects:
- `setTimeout`
- `clearTimeout`
- `setInterval`
- `clearInterval`
- `requestAnimationFrame`
- `random`
- `Date`
- `fetch`
- `localStorage`
+6 -41
View File
@@ -10,22 +10,13 @@
- [Static Properties](#static-properties) - [Static Properties](#static-properties)
- [Methods](#methods) - [Methods](#methods)
- [Lifecycle](#lifecycle) - [Lifecycle](#lifecycle)
- [`constructor(parent, props)`](#constructorparent-props)
- [`setup()`](#setup)
- [`willStart()`](#willstart)
- [`mounted()`](#mounted)
- [`willUpdateProps(nextProps)`](#willupdatepropsnextprops)
- [`willPatch()`](#willpatch)
- [`patched(snapshot)`](#patchedsnapshot)
- [`willUnmount()`](#willunmount)
- [`catchError(error)`](#catcherrorerror)
- [Root Component](#root-component) - [Root Component](#root-component)
- [Composition](#composition) - [Composition](#composition)
- [Form Input Bindings](#form-input-bindings) - [Form Input Bindings](#form-input-bindings)
- [References](#references) - [References](#references)
- [Dynamic sub components](#dynamic-sub-components) - [Dynamic sub components](#dynamic-sub-components)
- [Functional Components](#functional-components) - [Functional Components](#functional-components)
- [SVG Components](#svg-components) - [SVG components](#svg-components)
## Overview ## Overview
@@ -263,7 +254,7 @@ We explain here all the public methods of the `Component` class.
Note that if a component is mounted, unmounted and remounted, it will be 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 automatically re-rendered to ensure that changes in its state (or something
in the environment) will be taken into account. in the environment, or in the store, or ...) will be taken into account.
If a component is mounted inside an element or a fragment which is not in the If a component is mounted inside an element or a fragment which is not in the
DOM, then it will be rendered fully, but not active: the `mounted` hooks will DOM, then it will be rendered fully, but not active: the `mounted` hooks will
@@ -279,10 +270,6 @@ We explain here all the public methods of the `Component` class.
// app is now visible // app is now visible
``` ```
Note that the normal way of mounting an application is by using the `mount`
method on a component class, not by creating the instance by hand. See the
documentation on [mounting applications](mounting.md).
* **`unmount()`**: in case a component needs to be detached/removed from the DOM, this * **`unmount()`**: in case a component needs to be detached/removed from the DOM, this
method can be used. Most applications should not call `unmount`, this is more method can be used. Most applications should not call `unmount`, this is more
useful to the underlying component system. useful to the underlying component system.
@@ -298,13 +285,8 @@ We explain here all the public methods of the `Component` class.
are updated. It returns a boolean, which indicates if the component should are updated. It returns a boolean, which indicates if the component should
ignore a props update. If it returns false, then `willUpdateProps` will not ignore a props update. If it returns false, then `willUpdateProps` will not
be called, and no rendering will occur. Its default implementation is to be called, and no rendering will occur. Its default implementation is to
always return true. Note that this is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it 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. Since this is an can be useful if we are handling large number of components.
optimization, Owl has the freedom to ignore the result of `shouldUpdate` in
some cases (for example, if a component is remounted, or if we want to force
a full rerender of the UI). However, if `shouldUpdate` returns true, then Owl
provides the guarantee that the component will be rendered at some point in
the future (except if the component is destroyed or if some part of the UI crashes).
* **`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, and perform all necessary cleanup, such as unmounting the component, its children,
@@ -325,7 +307,7 @@ a owl component:
| Method | Description | | Method | Description |
| ------------------------------------------------ | ----------------------------------------------------------- | | ------------------------------------------------ | ----------------------------------------------------------- |
| **[setup](#setup)** | setup | | **[constructor](#constructorparent-props)** | constructor |
| **[willStart](#willstart)** | async, before first rendering | | **[willStart](#willstart)** | async, before first rendering |
| **[mounted](#mounted)** | just after component is rendered and added to the DOM | | **[mounted](#mounted)** | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update | | **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
@@ -370,23 +352,6 @@ class ClickCounter extends owl.Component {
} }
``` ```
Hook functions can be called in the constructor.
#### `setup()`
_setup_ is run just after the component is constructed. It is a lifecycle method,
very similar to the _constructor_, except that it does not receive any argument.
It is a valid method to call hook functions. Note that one of the main reason to
have the `setup` hook in the component lifecycle is to make it possible to
monkey patch it. It is a common need in the Odoo ecosystem.
```javascript
setup() {
useSetupAutofocus();
}
```
#### `willStart()` #### `willStart()`
willStart is an asynchronous hook that can be implemented to willStart is an asynchronous hook that can be implemented to
@@ -789,7 +754,7 @@ template rendered with `props`. In Owl, this can be done by
simply defining a template, that will access the `props` object: simply defining a template, that will access the `props` object:
```js ```js
const Welcome = xml`<h1>Hello, <t t-esc="props.name"/></h1>`; const Welcome = xml`<h1>Hello, {props.name}</h1>`;
class MyComponent extends Component { class MyComponent extends Component {
static template = xml` static template = xml`
+4 -2
View File
@@ -158,10 +158,12 @@ There are two different common problems with Owl asynchronous rendering model:
Here are a few tips on how to work with asynchronous components: Here are a few tips on how to work with asynchronous components:
1. Minimize the use of asynchronous components! 1. Minimize the use of asynchronous components!
2. Lazy loading external libraries is a good use case for async rendering. This 2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
synchronous renderings
3. Lazy loading external libraries is a good use case for async rendering. This
is mostly fine, because we can assume that it will only takes a fraction of a is mostly fine, because we can assume that it will only takes a fraction of a
second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs)) second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs))
3. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When 4. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When
this component is met, a new rendering this component is met, a new rendering
sub tree is created, such that the rendering of that component (and its sub tree is created, such that the rendering of that component (and its
children) is not tied to the rendering of the rest of the interface. It can children) is not tied to the rendering of the rest of the interface. It can
+17 -17
View File
@@ -7,30 +7,30 @@ For example, `Component` is available at `owl.Component` and `EventBus` is
exported as `owl.core.EventBus`. exported as `owl.core.EventBus`.
``` ```
browser
Component misc Component misc
Context AsyncRoot Context AsyncRoot
QWeb Portal QWeb Portal
mount Store router
useState tags useState Link
config css config RouteComponent
mode xml mode Router
core utils core tags
EventBus debounce EventBus css
Observer escape Observer xml
hooks loadJS hooks utils
onWillStart loadFile onWillStart debounce
onMounted shallowEqual onMounted escape
onWillUpdateProps whenReady onWillUpdateProps loadJS
onWillPatch onWillPatch loadFile
onPatched onPatched shallowEqual
onWillUnmount onWillUnmount whenReady
useContext useContext
useState useState
useRef useRef
useComponent
useEnv
useSubEnv useSubEnv
useStore
useDispatch
useGetters
``` ```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object. Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
+20 -5
View File
@@ -49,14 +49,15 @@ The correct way to customize an environment is to simply set it up on the root
component class, before the first component is created: component class, before the first component is created:
```js ```js
const env = { App.env = {
_t: myTranslateFunction, _t: myTranslateFunction,
user: {...}, user: {...},
services: { services: {
... ...
}, },
}; };
mount(App, { target: document.body, env }); const app = new App();
app.mount(document.body);
``` ```
It is also possible to simply share an environment between all root components, It is also possible to simply share an environment between all root components,
@@ -120,8 +121,9 @@ async function myEnv() {
} }
async function start() { async function start() {
const env = await myEnv(); App.env = await myEnv();
mount(App, { target: document.body, env }); const app = new App();
await app.mount(document.body);
} }
``` ```
@@ -133,4 +135,17 @@ the `QWeb` instance and a `browser` object:
- `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary - `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary
for Owl to be able to render anything for Owl to be able to render anything
- `browser`: this is an object that contains some common access points to the - `browser`: this is an object that contains some common access points to the
browser methods with a side effect. See [browser](browser.md) for more information. Note that the browser object will be removed from the environment in Owl 2.0. browser methods with a side effect. This is particularly useful when one want
to test more advanced components, and be able to mock those methods.
More specifically, the `browser` object contains the following methods and objects:
- `setTimeout`
- `clearTimeout`
- `setInterval`
- `clearInterval`
- `requestAnimationFrame`
- `random`
- `Date`
- `fetch`
- `localStorage`
+2
View File
@@ -23,3 +23,5 @@ Its API is:
| `off(eventType, owner)` | remove all listeners for an owner | | `off(eventType, owner)` | remove all listeners for an owner |
| `trigger(eventType, ...args)` | trigger an event | | `trigger(eventType, ...args)` | trigger an event |
| `clear` | remove all subscriptions | | `clear` | remove all subscriptions |
Note that the [`Store`](store.md) is an example of an `EventBus`.
+19 -18
View File
@@ -18,8 +18,9 @@
- [`useRef`](#useref) - [`useRef`](#useref)
- [`useSubEnv`](#usesubenv) - [`useSubEnv`](#usesubenv)
- [`useExternalListener`](#useexternallistener) - [`useExternalListener`](#useexternallistener)
- [`useComponent`](#usecomponent) - [`useStore`](#usestore)
- [`useEnv`](#useenv) - [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters)
- [Making customized hooks](#making-customized-hooks) - [Making customized hooks](#making-customized-hooks)
## Overview ## Overview
@@ -128,7 +129,7 @@ class SomeComponent extends Component {
### One rule ### One rule
There is only one rule: every hook for a component has to be called in the There is only one rule: every hook for a component has to be called in the
constructor, in the _setup_ method, or in class fields: constructor (or in class fields):
```js ```js
// ok // ok
@@ -144,13 +145,6 @@ class SomeComponent extends Component {
} }
} }
// also ok
class SomeComponent extends Component {
setup() {
this.state = useState({ value: 0 });
}
}
// not ok: this is executed after the constructor is called // not ok: this is executed after the constructor is called
class SomeComponent extends Component { class SomeComponent extends Component {
async willStart() { async willStart() {
@@ -372,15 +366,20 @@ to be closed:
useExternalListener(window, "click", this.closeMenu); useExternalListener(window, "click", this.closeMenu);
``` ```
### `useComponent` ### `useStore`
The `useComponent` hook is useful as a building block for some customized hooks, The `useStore` hook is the entry point for a component to connect to the store.
that may need a reference to the component calling them. See the [store documentation](store.md) for more information.
### `useEnv` ### `useDispatch`
The `useEnv` hook is useful as a building block for some customized hooks, The `useDispatch` hook is the way for components to get a reference to the store
that may need a reference to the env of the component calling them. `dispatch` function. See the [store documentation](store.md) for more information.
### `useGetters`
The `useGetters` hook is the way for components to get a reference to the store
getters. See the [store documentation](store.md) for more information.
### Making customized hooks ### Making customized hooks
@@ -436,11 +435,13 @@ not the solution to every problem.
```js ```js
function useRouter() { function useRouter() {
const env = useEnv(); return Component.current.env.router;
return env.router;
} }
``` ```
This means that we give control to the application developer to create the This means that we give control to the application developer to create the
router, which is good, so they can set it up, subclass it, ... And then, to router, which is good, so they can set it up, subclass it, ... And then, to
test our components, we can just add a mock router in the environment. test our components, we can just add a mock router in the environment.
Note: the code above makes use of the `Component.current` property. This is the
way hooks are able to get a reference to the component currently being created.
+3 -2
View File
@@ -43,7 +43,7 @@ workflow to help the user put in some data, which it could use later on.
JavaScript: JavaScript:
```js ```js
const { Component, mount } = owl; const { Component } = owl;
const { Portal } = owl.misc; const { Portal } = owl.misc;
class TeleportedComponent extends Component {} class TeleportedComponent extends Component {}
@@ -51,7 +51,8 @@ class App extends Component {
static components = { Portal, TeleportedComponent }; static components = { Portal, TeleportedComponent };
} }
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
``` ```
XML: XML:
-60
View File
@@ -1,60 +0,0 @@
# 🦉 Mounting an application 🦉
## Content
- [Overview](#overview)
- [API](#api)
## Overview
Mounting an Owl application is done by using the `mount` method (available in
`owl.mount` if you are using the iife build, or it can be directly imported
from `owl` if you are using a module system):
```js
const mount = { owl }; // if owl is available as an object
const env = { ... };
const app = await mount(MyComponent, { target: document.body, env });
```
Another example:
```js
const config = {
env: ...,
props: ...,
target: document.body,
position: "self",
};
const app = await mount(App, config);
```
A common way to initialize an application is to first setup an environment,
then to call the `mount` method.
## API
Mount takes two parameters:
- `C`, which should be a component class (NOT instance),
- `params`, which is an object with the following keys:
- `target (HTMLElement | DocumentFragment)`: the target of the mount operation
- `env (optional, Env)` an environment
- `position (optional, "first-child" | "last-child" | "self")` the position
where it should be mounted (see below for more informations)
- `props (optional, any)`: some initial values that are given as props. Useful
when the root component is configurable, or when testing sub components
Here are the various positions supported by Owl:
- `first-child`: with this option, the component will be prepended inside the target,
- `last-child` (default value): with this option, the component will be
appended in the target element,
- `self`: the target will be used as the root element for the component. This
means that the target has to be an HTMLElement (and not a document fragment).
In this situation, it is possible that the component cannot be unmounted. For
example, if its target is `document.body`.
The `mount` method returns a promise that resolves to the instance of the created
component.
+5 -5
View File
@@ -15,7 +15,7 @@ use cases, there is no need to directly instantiate an observer.
For example, this code will display `update` in the console: For example, this code will display `update` in the console:
```javascript ```javascript
const observer = new owl.core.Observer(); const observer = new owl.Observer();
observer.notifyCB = () => console.log("update"); observer.notifyCB = () => console.log("update");
const obj = observer.observe({ a: { b: 1 } }); const obj = observer.observe({ a: { b: 1 } });
@@ -39,14 +39,14 @@ is incremented every time the value is observed. Sometimes, it can be useful
to obtain that number: to obtain that number:
```js ```js
const observer = new owl.core.Observer(); const observer = new owl.Observer();
const obj = observer.observe({ a: { b: 1 } }); const obj = observer.observe({ a: { b: 1 } });
observer.revNumber(obj.a); // 1 observer.deepRevNumber(obj.a); // 1
obj.a.b = 2; obj.a.b = 2;
observer.revNumber(obj.a); // 2 observer.deepRevNumber(obj.a); // 2
``` ```
The `revNumber` can also return 0, which indicates that the value is not The `deepRevNumber` can also return 0, which indicates that the value is not
observed. observed.
+1 -1
View File
@@ -18,7 +18,7 @@ class Child extends Component {
} }
class Parent extends Component { class Parent extends Component {
static template = xml`<div><Child a="state.a" b="'string'"/></div>`; static template = xml`<div><ComponentA a="state.a" b="'string'"/></div>`;
static components = { Child }; static components = { Child };
state = useState({ a: "fromparent" }); state = useState({ a: "fromparent" });
} }
-37
View File
@@ -13,8 +13,6 @@
- [Setting Variables](#setting-variables) - [Setting Variables](#setting-variables)
- [Conditionals](#conditionals) - [Conditionals](#conditionals)
- [Dynamic Attributes](#dynamic-attributes) - [Dynamic Attributes](#dynamic-attributes)
- [Dynamic Class Attribute](#dynamic-class-attribute)
- [Dynamic Tag Names](#dynamic-tag-names)
- [Loops](#loops) - [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates) - [Rendering Sub Templates](#rendering-sub-templates)
- [Dynamic Sub Templates](#dynamic-sub-templates) - [Dynamic Sub Templates](#dynamic-sub-templates)
@@ -78,7 +76,6 @@ needs. Here is a list of all Owl specific directives:
| `t-transition` | [Defining an animation](animations.md#css-transitions) | | `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-slot` | [Rendering a slot](slots.md) | | `t-slot` | [Rendering a slot](slots.md) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) | | `t-model` | [Form input bindings](component.md#form-input-bindings) |
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
## Reference ## Reference
@@ -327,40 +324,6 @@ values) or a pair `[key, value]`. For example:
<div t-att="['a', 'b']"/> <!-- <div a="b"></div> --> <div t-att="['a', 'b']"/> <!-- <div a="b"></div> -->
``` ```
### Dynamic class attribute
For convenience, Owl supports a special case for the `t-att-class` case: one can
use an object with keys describing the classes, and values boolean value denoting
if the class is or is not present:
```xml
<div t-att-class="{'a': true, 'b': true}"/> <!-- result: <div class="a b"></div> -->
<div t-att-class="{'a b': true, 'c': true}"/> <!-- result: <div class="a b c"></div> -->
```
Note that it can be combined with normal class attribute:
```xml
<div class="a" t-att-class="{'b': true}"/> <!-- result: <div class="a b"></div> -->
```
### Dynamic tag names
When writing generic components or templates, the specific concrete tag for an
HTML element is not known yet. In those situations, the `t-tag` directive is
useful. It simply evaluates dynamically an expression to use as a tag name. The
template:
```xml
<t t-tag="tag">
<span>content</span>
</t>
```
will be rendered as `<div><span>content</span></div>` if the `tag` context key
is set to `div`.
### Loops ### Loops
QWeb has an iteration directive `t-foreach` which take an expression returning the QWeb has an iteration directive `t-foreach` which take an expression returning the
+183
View File
@@ -0,0 +1,183 @@
# 🦉 Router 🦉
Remove?
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Route Definition](#route-definition)
- [Router](#router)
- [Navigation Guards](#navigation-guards)
- [RouteComponent](#routecomponent)
- [Link](#link)
## Overview
It is often useful to organize an application around urls. If the application is
a single page application, then we need a way to manage those urls in the browser.
This is why there are many different routers for different frameworks. A generic
router can do the job just fine, but a specialized router for Owl can give a
better developer experience.
The Owl router support the following features:
- `history` or `hash` mode
- declarative routes
- route redirection
- navigation guards
- parameterized routes
- a `<Link/>` component
- a `<RouteComponent/>` component
Note that it is still in early stage of developments, and there are probably
still some issues.
## Example
To use the Owl router, there are some steps that needs to be done:
- declare some routes
- create a router
- add it to the environment
```js
async function protectRoute({ env, to }) {
if (!env.session.authUser) {
env.session.setNextRoute(to.name);
return { to: "SIGN_IN" };
}
return true;
}
export const ROUTES = [
{ name: "LANDING", path: "/", component: Landing },
{ name: "TASK", path: "/tasks/{{id}}", component: Task },
{ name: "SIGN_UP", path: "/signup", component: SignUp },
{ name: "SIGN_IN", path: "/signin", component: SignIn },
{ name: "ADMIN", path: "/admin", component: Admin, beforeRouteEnter: protectRoute },
{ name: "ACCOUNT", path: "/account", component: Account, beforeRouteEnter: protectRoute },
{ name: "UNKNOWN", path: "*", redirect: { to: "LANDING" } }
];
function makeEnvironment() {
...
const env = { qweb };
env.session = new Session(env);
env.router = new owl.router.Router(env, ROUTES);
await env.router.start();
return env;
}
App.env = makeEnvironment();
// create root component here
```
Notice that the router needs to be started. This is an asynchronous operation
because it needs to apply the potential navigation guards on the current route
(which may or may not mean that the application is redirected to another route).
## Reference
### Route definition
A route need to be defined as an object with the following keys:
- `name` (optional): a (unique) string useful to identify the current route. If not
given, it will be assigned an automatic name,
- `path`: a string describing the url. It can be static: `/admin` or dynamic: `/users/{{id}}`.
It also can be `*`, to catch all remaining routes.
- `component` (optional): an Owl component that will be used by the `t-routecomponent`
directive if the route is active
- `redirect` (optional): should be destination object (with optional keys `path`, `to` and `params`) if given, the application will be redirected to the destination whenever we match this route
- `beforeRouteEnter`: defines a [navigation guard](#navigation-guards).
### `Router`
The `Router` constructor takes three arguments:
- `env`: a valid environment,
- a list of routes,
- an optional object (with the only key `mode` which can be `history` (default
value) or `hash`).
`history` will use the browser [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) as the mechanism to manage URL.\
Example: `https://yourdomain.tld/my_custom_route`.\
For this mechanism to work, you need a way to configure your web server accordingly.
`hash` will manipulate the hash of the URL.\
Example: `https://yourdomain.tld/index.html#/my_custom_route`.
```js
const ROUTES = [...];
const router = new owl.router.Router(env, ROUTES, {mode: 'history'});
```
Note that the route are defined in a list, and the order matters: the router
tries to find a match by going down the list.
The router needs to be added to the environment in the `router` sub key.
Once a router is created, it needs to be started. This is necessary to initialize
its current state to the current URL (and also, to potentially apply any
navigation guards and/or redirecting).
```js
await router.start();
```
Once started, the router will keep track of the current url and reflect its
value in two keys:
- `router.currentRoute`
- `router.currentParams`
The router also has a `navigate` method, useful to programmatically change the
application to another state (and the url):
```js
router.navigate({ to: "USER", params: { id: 51 } });
```
### Navigation Guards
Navigation guards are very useful to be able to execute some business logic/
perform some actions or redirect to other routes whenever the application is
entering a new route. For example, the following guard checks if there is an
authenticated user, and if it is not the case, redirect to the sign in route.
```js
async function protectRoute({ env, to }) {
if (!env.session.authUser) {
env.session.setNextRoute(to.name);
return { to: "SIGN_IN" };
}
return true;
}
```
A navigation guard is a function that returns a promise, which either resolves
to `true` (the navigation is accepted), or to another destination object.
### `RouteComponent`
The `RouteComponent` component directs Owl to render the component associated
to the currently active route (if any):
```xml
<div t-name="App">
<NavBar />
<RouteComponent />
</div>
```
### `Link`
The `Link` component is a Owl component which render as a `<a>` tag with any
content. It will compute the proper href from its props, and allow Owl to
properly navigate to a given url if clicked on it.
```xml
<Link to="'HOME'">Home</Link>
```
+3 -18
View File
@@ -25,8 +25,6 @@ some sub template, but still be the owner. For example, a generic dialog compone
will need to render some content, some footer, but with the parent as the will need to render some content, some footer, but with the parent as the
rendering context. rendering context.
Slots are inserted with the `t-slot` directive:
```xml ```xml
<div t-name="Dialog" class="modal"> <div t-name="Dialog" class="modal">
<div class="modal-title"><t t-esc="props.title"/></div> <div class="modal-title"><t t-esc="props.title"/></div>
@@ -44,7 +42,7 @@ Slots are defined by the caller, with the `t-set-slot` directive:
```xml ```xml
<div t-name="SomeComponent"> <div t-name="SomeComponent">
<div>some component</div> <div>some component</div>
<Dialog title="'Some Dialog'"> <Dialog title="Some Dialog">
<t t-set-slot="content"> <t t-set-slot="content">
<div>hey</div> <div>hey</div>
</t> </t>
@@ -64,9 +62,7 @@ This is deprecated and should no longer be used in new code.
## Reference ## Reference
### Default Slot Default slot: the first element inside the component which is not a named slot will
The first element inside the component which is not a named slot will
be considered the `default` slot. For example: be considered the `default` slot. For example:
```xml ```xml
@@ -81,9 +77,7 @@ be considered the `default` slot. For example:
</div> </div>
``` ```
### Default content Default content: slots can define a default content, in case the parent did not define them:
Slots can define a default content, in case the parent did not define them:
```xml ```xml
<div t-name="Parent"> <div t-name="Parent">
@@ -100,12 +94,3 @@ Rendering context: the content of the slots is actually rendered with the
rendering context corresponding to where it was defined, not where it is rendering context corresponding to where it was defined, not where it is
positioned. This allows the user to define event handlers that will be bound positioned. This allows the user to define event handlers that will be bound
to the correct component (usually, the grandparent of the slot content). to the correct component (usually, the grandparent of the slot content).
### Dynamic Slots
The `t-slot` directive is actually able to use any expressions, using string
interplolation:
```xml
<t t-slot="{{current}}" />
```
+349
View File
@@ -0,0 +1,349 @@
# 🦉 Store 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Store](#store)
- [Actions](#actions)
- [Getters](#getters)
- [Connecting a Component](#connecting-a-component)
- [`useStore`](#usestore)
- [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters)
- [Semantics](#semantics)
- [Good Practices](#good-practices)
## Overview
Managing the state in an application is not an easy task. In some cases, the
state of an application can be part of the component tree, in a natural way.
However, there are situations where some parts of the state need to be displayed
in various parts of the user interface, and then, it is not obvious which
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 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.
## Example
Here is what a simple store looks like:
```js
const actions = {
addTodo({ state }, message) {
state.todos.push({
id: state.nextId++,
message,
isCompleted: false,
});
},
};
const state = {
todos: [],
nextId: 1,
};
const store = new owl.Store({ state, actions });
store.on("update", null, () => console.log(store.state));
// updating the state
store.dispatch("addTodo", "fix all bugs");
```
This example shows how a store can be defined and used. Note that in most cases,
actions will be dispatched by connected components.
## Reference
### `Store`
The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events
whenever its state is changed. Note that these events are triggered only after a
microtask tick, so only one event will be triggered for any number of state changes in a
call stack.
Also, it is important to mention that the state is observed (with an `owl.Observer`),
which is the reason why it is able to know if it was changed. See the
[Observer](observer.md)'s documentation for more details.
The `Store` class is quite small. It has two public methods:
- its constructor
- `dispatch`
The constructor takes a configuration object with four (optional) keys:
- the initial state
- the actions
- the getters
- the environment
```javascript
const config = {
state,
actions,
getters,
env,
};
const store = new Store(config);
```
### Actions
Actions are used to coordinate state changes. It can be used for both synchronous
and asynchronous logic.
```js
const actions = {
async login({ state }, info) {
state.loginState = "pending";
try {
const loginInfo = await doSomeRPC("/login/", info);
state.loginState = loginInfo;
} catch (e) {
state.loginState = "error";
}
},
};
```
The first argument to an action method is an object with four keys:
- `state`: the current state of the store content,
- `dispatch`: a function that can be used to dispatch other actions,
- `getters`: an object containing all getters defined in the store,
- `env`: the current environment. This is useful sometimes, in particular if
an action needs to apply some side effects (such as performing an rpc), and
the `rpc` method is located in the environment.
Actions are called with the `dispatch` method on the store, and can receive an
arbitrary number of arguments.
```js
store.dispatch("login", someInfo);
```
Note that anything returned by an action will also be returned by the `dispatch`
call.
Also, it is important to be aware that we need to be careful with asynchronous
logic. Each state change will potentially trigger a rerendering, so we need to
make sure that we do not have a partially corrupted state. Here is an example that
is likely not a good idea:
```javascript
const actions = {
async fetchSomeData({ state }, recordId) {
state.recordId = recordId;
const data = await doSomeRPC("/read/", recordId);
state.recordData = data;
},
};
```
In the previous example, there is a period of time in which the state has a
`recordId` which does not correspond to the `recordData`. It is more likely that
we want an atomic update: updating the `recordId` at the same time as the `recordData`
values:
```javascript
const actions = {
async fetchSomeData({ state }, recordId) {
const data = await doSomeRPC("/read/", recordId);
state.recordId = recordId;
state.recordData = data;
},
};
```
### Getters
Usually, data contained in the store will be stored in a normalized way. For
example,
```js
{
posts: [{id: 11, authorId: 4, content: 'Greetings'}],
authors: [{id: 4, name: 'John'}]
}
```
However, the user interface will probably need some denormalized data like
```js
{id: 11, author: {id: 4, name: 'John'}, content: 'Greetings'}
```
This is what `getters` are for: they give a centralized way to process and
transform the data contained in the store.
```js
const getters = {
getPost({ state }, id) {
const post = state.posts.find((p) => p.id === id);
const author = state.authors.find((a) => a.id === post.id);
return {
id,
author,
content: post.content,
};
},
};
// somewhere else
const post = store.getters.getPost(id);
```
Getters take _at most_ one argument.
Note that getters are not cached.
### Connecting a Component
At some point, we need a way to interact with the store from a component. This
means that the component needs a reference to the store. By default, it looks
for it in the `env.store` key. However, this can be configured with the `useStore`
hook.
Every component-store interactions are done with the help of the three store hooks:
- [`useStore`](#usestore) to subscribe a component to some part of the store state,
- [`useDispatch`](#usedispatch) to get a reference to a dispatch function,
- [`useGetters`](#usegetters) to get a reference to the getters defined in the store.
Assume we have this store:
```javascript
const actions = {
increment({ state }, val) {
state.counter.value += val;
},
};
const state = {
counter: { value: 0 },
};
const store = new owl.Store({ state, actions });
```
To make it accessible to the complete application, we will put it in the
environment:
```js
// in this example, the root component is App
App.env.store = store;
```
A counter component can then select this value and dispatch an action like this:
```js
class Counter extends Component {
counter = useStore((state) => state.counter);
dispatch = useDispatch();
}
const counter = new Counter({ store, qweb });
```
```xml
<button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="counter.value"/>]
</button>
```
### `useStore`
The `useStore` hook is used to select some part of the store state. It accepts
two arguments:
- a selector function, which takes the store state as first argument (and the
component props as second argument) and which must return the part of the
store state that will be made available and observed for changes,
- optionally, an object which can have the following optional keys:
- a `store` key containing a store object if we want to use another store than
the default store,
- an `isEqual` key containing an equality function if we want to specialize
the comparison (the function must accept two arguments: the previous result
and the new result, and must return whether they are equal),
- and an `onUpdate` key containing an update function if we want to execute an
arbitrary code every time the selected state changes (the function will
receive one argument, the new result, and can execute arbitrary code).
If the `useStore` selector returns a sub part of the store state, the component
will only be rerendered whenever this part of the state changes. Otherwise, it
will perform a strict equality check (unless the `isEqual` option is defined,
then it will call it) and will update the component every time this check fails.
Note that if the selector function returns a primitive type, the result of
`useStore` will be immutable and it will not react to changes. In this case, it
is important to define the `onUpdate` option to properly update the value
manually when it changes.
Also, the return value from `useStore` is not supposed to be modified. The store
state should only be updated with actions.
### `useDispatch`
The `useDispatch` hook is useful when a component needs to be able to dispatch
actions. It takes an optional argument, which is a store. If not given, it will
use the store in the environment.
Note that a component does not need to be connected in any other way to the store.
For example:
```js
class DoSomethingButton extends Component {
static template = xml`<button t-on-click="dispatch('something')">Click</button>`;
dispatch = useDispatch();
}
```
### `useGetters`
The `useGetters` hook is useful when a component needs to be able to use the
getters defined in a store. It takes an optional argument, which is a store. If
not given, it will use the store in the environment.
Note that a component does not need to be connected in any other way to the store.
For example:
```js
class InfoButton extends Component {
static template = xml`<span><t t-esc="getters.somevalue()"></span>`;
getters = useGetters();
}
```
### Semantics
The `Store` class and the `useStore` hook try to be smart and to optimize as much
as possible the rendering and update process. What is important to know is:
- components are always updated in the order of their creation (so, parent
before children),
- they are updated only if they are in the DOM,
- if a parent is asynchronous, the system will wait for it to complete its
update before updating other components,
- in general, updates are not coordinated. This is not a problem for synchronous
components, but if there are many asynchronous components, this could lead to
a situation where some part of the UI is updated and some other part of the UI is
not updated.
### Good Practices
- avoid asynchronous components as much as possible. Asynchronous components
lead to situations where parts of the UI is not updated immediately,
- do not be afraid to connect many components, parent or children if needed. For
example, a `MessageList` component could get a list of ids in its `useStore`
call and a `Message` component could get the data of its own
message,
- since the `useStore` function is called for each connected component,
for each state update, it is important to make sure that these functions are
as fast as possible.
+6 -6
View File
@@ -62,14 +62,14 @@ The CSS tag is useful to define a css stylesheet in the javascript file:
```js ```js
class MyComponent extends Component { class MyComponent extends Component {
static template = xml` static template = xml`
<div class="my-component">some template</div> <div class="my-component">some template</div>
`; `;
static style = css` static css`
.my-component { .my-component {
color: red; color: red;
} }
`; `;
} }
``` ```
+4 -4
View File
@@ -21,8 +21,8 @@ argument, it executes it as soon as the DOM ready (or directly).
```js ```js
Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function ([templates]) { Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function ([templates]) {
const qweb = new owl.QWeb({ templates }); const qweb = new owl.QWeb({ templates });
const env = { qweb }; const app = new App({ qweb });
await mount(App, { env, target: document.body }); app.mount(document.body);
}); });
``` ```
@@ -31,8 +31,8 @@ or alternatively:
```js ```js
owl.utils.whenReady(function () { owl.utils.whenReady(function () {
const qweb = new owl.QWeb(); const qweb = new owl.QWeb();
const env = { qweb }; const app = new App({ qweb });
await mount(App, { env, target: document.body }); app.mount(document.body);
}); });
``` ```
+10 -16
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "2.0.0-alpha1", "version": "1.0.13",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js", "main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js", "browser": "dist/owl.iife.js",
@@ -10,20 +10,18 @@
"dist" "dist"
], ],
"engines": { "engines": {
"node": ">=12.18.3" "node": ">=10.15.3"
}, },
"scripts": { "scripts": {
"build:bundle": "rollup -c", "build:bundle": "rollup -c",
"build": "npm run build:bundle", "build": "npm run build:bundle",
"test": "jest", "test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"playground:serve": "python3 tools/server.py || python tools/server.py", "tools:serve": "python3 tools/server.py || python tools/server.py",
"playground": "npm run build && npm run playground:serve", "tools": "npm run build && npm run tools:serve",
"preplayground:watch": "npm run build", "pretools:watch": "npm run build",
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"", "tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write", "prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --check",
"publish": "npm run build && npm publish", "publish": "npm run build && npm publish",
"release": "node tools/release.js" "release": "node tools/release.js"
}, },
@@ -42,22 +40,20 @@
"@types/node": "^14.11.8", "@types/node": "^14.11.8",
"chalk": "^3.0.0", "chalk": "^3.0.0",
"cpx": "^1.5.0", "cpx": "^1.5.0",
"current-git-branch": "^1.1.0",
"git-rev-sync": "^1.12.0", "git-rev-sync": "^1.12.0",
"github-api": "^3.3.0", "github-api": "^3.3.0",
"jest": "^27.1.0", "jest": "^27.1.0",
"jest-diff": "^27.3.1",
"jest-environment-jsdom": "^27.1.0", "jest-environment-jsdom": "^27.1.0",
"live-server": "^1.2.1", "live-server": "^1.2.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "2.4.1", "prettier": "^2.0.4",
"rollup": "^2.56.3", "rollup": "^2.56.3",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.31.1", "rollup-plugin-typescript2": "^0.30.0",
"sass": "^1.16.1", "sass": "^1.16.1",
"source-map-support": "^0.5.10", "source-map-support": "^0.5.10",
"ts-jest": "^27.0.5", "ts-jest": "^27.0.5",
"typescript": "4.5.2", "typescript": "3.9.6",
"uglify-es": "^3.3.9" "uglify-es": "^3.3.9"
}, },
"jest": { "jest": {
@@ -66,9 +62,7 @@
"<rootDir>/src", "<rootDir>/src",
"<rootDir>/tests" "<rootDir>/tests"
], ],
"setupFiles": [ "setupFiles": ["./tests/mocks/mockEventTarget.js"],
"./tests/mocks/mockEventTarget.js"
],
"transform": { "transform": {
"^.+\\.ts?$": "ts-jest" "^.+\\.ts?$": "ts-jest"
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉 # 🦉 OWL Roadmap 🦉
- Current version: 1.4.7 - Current version: 1.0.13
- Status: stable - Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may This roadmap is only an attempt at predicting Owl's future. Everything may
+45
View File
@@ -0,0 +1,45 @@
import { Component } from "./component/component";
import { ComponentNode } from "./component/component_node";
import { Scheduler } from "./component/scheduler";
import { TemplateSet } from "./qweb/template_helpers";
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
export class App<T extends typeof Component = any> extends TemplateSet {
Root: T;
props: any;
env: any = {};
scheduler = new Scheduler(window.requestAnimationFrame.bind(window));
root: ComponentNode | null = null;
dev: boolean = true;
constructor(Root: T, props?: any) {
super();
this.Root = Root;
this.props = props;
}
configure(params: any) {
if (params.env) {
this.env = params.env;
}
}
mount(target: HTMLElement): Promise<InstanceType<T>> {
if (!(target instanceof HTMLElement)) {
throw new Error("Cannot mount component: the target is not a valid DOM element");
}
if (!document.body.contains(target)) {
throw new Error("Cannot mount a component on a detached dom node");
}
const node = new ComponentNode(this.Root, this.props, this);
this.root = node;
return node.mountComponent(target);
}
destroy() {
if (this.root) {
this.root.destroy();
}
}
}
-105
View File
@@ -1,105 +0,0 @@
import { Component } from "../component/component";
import { ComponentNode } from "../component/component_node";
import { MountOptions } from "../component/fibers";
import { Scheduler } from "../component/scheduler";
import { TemplateSet, TemplateSetConfig } from "./template_set";
import { nodeErrorHandlers } from "../component/error_handling";
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
export interface Env {
[key: string]: any;
}
export interface AppConfig extends TemplateSetConfig {
env?: Env;
props?: any;
}
export const DEV_MSG = `Owl is running in 'dev' mode.
This is not suitable for production use.
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`;
export class App<T extends typeof Component = any> extends TemplateSet {
Root: T;
props: any;
env: Env;
scheduler = new Scheduler(window.requestAnimationFrame.bind(window));
root: ComponentNode | null = null;
constructor(Root: T, config: AppConfig = {}) {
super(config);
this.Root = Root;
if (config.dev) {
console.info(DEV_MSG);
}
const descrs = Object.getOwnPropertyDescriptors(config.env || {});
this.env = Object.freeze(Object.defineProperties({}, descrs));
this.props = config.props || {};
}
mount(target: HTMLElement, options?: MountOptions): Promise<InstanceType<T>> {
this.checkTarget(target);
const node = this.makeNode(this.Root, this.props);
const prom = this.mountNode(node, target, options);
this.root = node;
return prom;
}
checkTarget(target: HTMLElement) {
if (!(target instanceof HTMLElement)) {
throw new Error("Cannot mount component: the target is not a valid DOM element");
}
if (!document.body.contains(target)) {
throw new Error("Cannot mount a component on a detached dom node");
}
}
makeNode(Component: T, props: any): ComponentNode {
return new ComponentNode(Component, props, this);
}
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
const promise: any = new Promise((resolve, reject) => {
let isResolved = false;
// manually set a onMounted callback.
// that way, we are independant from the current node.
node.mounted.push(() => {
resolve(node.component);
isResolved = true;
});
// Manually add the last resort error handler on the node
let handlers = nodeErrorHandlers.get(node);
if (!handlers) {
handlers = [];
nodeErrorHandlers.set(node, handlers);
}
handlers.unshift((e) => {
if (isResolved) {
console.error(e);
} else {
reject(e);
}
throw e;
});
});
node.mountComponent(target, options);
return promise;
}
destroy() {
if (this.root) {
this.root.destroy();
}
}
}
export async function mount<T extends typeof Component>(
C: T,
target: HTMLElement,
config: AppConfig & MountOptions = {}
): Promise<InstanceType<T>> {
return new App(C, config).mount(target, config);
}
-213
View File
@@ -1,213 +0,0 @@
import { BDom, multi, text, toggler } from "../blockdom";
import { validateProps } from "../component/props_validation";
import { Markup } from "../utils";
import { html } from "../blockdom/index";
import { VPortal } from "../portal";
/**
* This file contains utility functions that will be injected in each template,
* to perform various useful tasks in the compiled code.
*/
function withDefault(value: any, defaultValue: any): any {
return value === undefined || value === null || value === false ? defaultValue : value;
}
function callPortal(
ctx: any,
parent: any,
key: string,
target: string,
content: (ctx: any, node: any, key: string) => BDom
): BDom {
const portal = new VPortal(target, content(ctx, parent, key), ctx.__owl__) as any;
return portal;
}
function callSlot(
ctx: any,
parent: any,
key: string,
name: string,
dynamic: boolean,
extra: any,
defaultContent?: (ctx: any, node: any, key: string) => BDom
): BDom {
const slots = (ctx.props && ctx.props.slots) || {};
const { __render, __ctx, __scope } = slots[name] || {};
const slotScope = Object.create(__ctx || {});
if (__scope) {
slotScope[__scope] = extra || {};
}
const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
if (defaultContent) {
let child1: BDom | undefined = undefined;
let child2: BDom | undefined = undefined;
if (slotBDom) {
child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
} else {
child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
}
return multi([child1, child2]);
}
return slotBDom || text("");
}
function capture(ctx: any): any {
const component = ctx.__owl__.component;
const result = Object.create(component);
for (let k in ctx) {
result[k] = ctx[k];
}
return result;
}
function withKey(elem: any, k: string) {
elem.key = k;
return elem;
}
function prepareList(collection: any): [any[], any[], number, any[]] {
let keys: any[];
let values: any[];
if (Array.isArray(collection)) {
keys = collection;
values = collection;
} else if (collection) {
values = Object.keys(collection);
keys = Object.values(collection);
} else {
throw new Error("Invalid loop expression");
}
const n = values.length;
return [keys, values, n, new Array(n)];
}
const isBoundary = Symbol("isBoundary");
function setContextValue(ctx: { [key: string]: any }, key: string, value: any): void {
const ctx0 = ctx;
while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
const newCtx = ctx.__proto__;
if (!newCtx) {
ctx = ctx0;
break;
}
ctx = newCtx;
}
ctx[key] = value;
}
function toNumber(val: string): number | string {
const n = parseFloat(val);
return isNaN(n) ? val : n;
}
function shallowEqual(l1: any[], l2: any[]): boolean {
for (let i = 0, l = l1.length; i < l; i++) {
if (l1[i] !== l2[i]) {
return false;
}
}
return true;
}
class LazyValue {
fn: any;
ctx: any;
node: any;
constructor(fn: any, ctx: any, node: any) {
this.fn = fn;
this.ctx = capture(ctx);
this.node = node;
}
evaluate(): any {
return this.fn(this.ctx, this.node);
}
toString() {
return this.evaluate().toString();
}
}
/*
* Safely outputs `value` as a block depending on the nature of `value`
*/
export function safeOutput(value: any): ReturnType<typeof toggler> {
if (!value) {
return value;
}
let safeKey;
let block;
if (value instanceof Markup) {
safeKey = `string_safe`;
block = html(value as string);
} else if (value instanceof LazyValue) {
safeKey = `lazy_value`;
block = value.evaluate();
} else if (typeof value === "string") {
safeKey = "string_unsafe";
block = text(value);
} else {
// Assuming it is a block
safeKey = "block_safe";
block = value;
}
return toggler(safeKey, block);
}
let boundFunctions = new WeakMap();
function bind(ctx: any, fn: Function): Function {
let component = ctx.__owl__.component;
let boundFnMap = boundFunctions.get(component);
if (!boundFnMap) {
boundFnMap = new WeakMap();
boundFunctions.set(component, boundFnMap);
}
let boundFn = boundFnMap.get(fn);
if (!boundFn) {
boundFn = fn.bind(component);
boundFnMap.set(fn, boundFn);
}
return boundFn;
}
type RefMap = { [key: string]: HTMLElement | null };
type RefSetter = (el: HTMLElement | null) => void;
function multiRefSetter(refs: RefMap, name: string): RefSetter {
let count = 0;
return (el) => {
if (el) {
count++;
if (count > 1) {
throw new Error("Cannot have 2 elements with same ref name at the same time");
}
}
if (count === 0 || el) {
refs[name] = el;
}
};
}
export const UTILS = {
withDefault,
zero: Symbol("zero"),
isBoundary,
callSlot,
callPortal,
capture,
withKey,
prepareList,
setContextValue,
multiRefSetter,
shallowEqual,
toNumber,
validateProps,
LazyValue,
safeOutput,
bind,
};
-126
View File
@@ -1,126 +0,0 @@
import { createBlock, html, list, multi, text, toggler, comment } from "../blockdom";
import { compile, Template } from "../compiler";
import { component } from "../component/component_node";
import { UTILS } from "./template_helpers";
const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
export const globalTemplates: { [key: string]: string | Node } = {};
function parseXML(xml: string): Document {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) {
let msg = "Invalid XML in template.";
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
if (parsererrorText) {
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
const re = /\d+/g;
const firstMatch = re.exec(parsererrorText);
if (firstMatch) {
const lineNumber = Number(firstMatch[0]);
const line = xml.split("\n")[lineNumber - 1];
const secondMatch = re.exec(parsererrorText);
if (line && secondMatch) {
const columnIndex = Number(secondMatch[0]) - 1;
if (line[columnIndex]) {
msg +=
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
`${line}\n${"-".repeat(columnIndex - 1)}^`;
}
}
}
}
throw new Error(msg);
}
return doc;
}
export interface TemplateSetConfig {
dev?: boolean;
translatableAttributes?: string[];
translateFn?: (s: string) => string;
templates?: string | Document;
}
export class TemplateSet {
dev: boolean;
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
templates: { [name: string]: Template } = {};
translateFn?: (s: string) => string;
translatableAttributes?: string[];
utils: typeof UTILS = Object.assign({}, UTILS, {
call: (owner: any, subTemplate: string, ctx: any, parent: any, key: any) => {
const template = this.getTemplate(subTemplate);
return toggler(subTemplate, template.call(owner, ctx, parent, key));
},
getTemplate: (name: string) => this.getTemplate(name),
});
constructor(config: TemplateSetConfig = {}) {
this.dev = config.dev || false;
this.translateFn = config.translateFn;
this.translatableAttributes = config.translatableAttributes;
if (config.templates) {
this.addTemplates(config.templates);
}
}
addTemplate(name: string, template: string | Node, options: { allowDuplicate?: boolean } = {}) {
if (name in this.rawTemplates && !options.allowDuplicate) {
throw new Error(`Template ${name} already defined`);
}
this.rawTemplates[name] = template;
}
addTemplates(xml: string | Document, options: { allowDuplicate?: boolean } = {}) {
if (!xml) {
// empty string
return;
}
xml = xml instanceof Document ? xml : parseXML(xml);
for (const template of xml.querySelectorAll("[t-name]")) {
const name = template.getAttribute("t-name")!;
template.removeAttribute("t-name");
this.addTemplate(name, template, options);
}
}
getTemplate(name: string): Template {
if (!(name in this.templates)) {
const rawTemplate = this.rawTemplates[name];
if (rawTemplate === undefined) {
throw new Error(`Missing template: "${name}"`);
}
const templateFn = this._compileTemplate(name, rawTemplate);
// first add a function to lazily get the template, in case there is a
// recursive call to the template name
this.templates[name] = (context, parent) => this.templates[name](context, parent);
const template = templateFn(bdom, this.utils);
this.templates[name] = template;
}
return this.templates[name];
}
_compileTemplate(name: string, template: string | Node) {
return compile(template, {
name,
dev: this.dev,
translateFn: this.translateFn,
translatableAttributes: this.translatableAttributes,
});
}
}
// -----------------------------------------------------------------------------
// xml tag helper
// -----------------------------------------------------------------------------
export function xml(...args: Parameters<typeof String.raw>) {
const name = `__template__${xml.nextId++}`;
const value = String.raw(...args);
globalTemplates[name] = value;
return name;
}
xml.nextId = 1;
+7 -19
View File
@@ -1,6 +1,6 @@
import type { Setter } from "./block_compiler"; import type { Setter } from "./block_compiler";
const { setAttribute: elemSetAttribute, removeAttribute } = Element.prototype; const { setAttribute, removeAttribute } = Element.prototype;
const tokenList = DOMTokenList.prototype; const tokenList = DOMTokenList.prototype;
const tokenListAdd = tokenList.add; const tokenListAdd = tokenList.add;
const tokenListRemove = tokenList.remove; const tokenListRemove = tokenList.remove;
@@ -14,23 +14,11 @@ const wordRegexp = /\s+/;
* file. * file.
*/ */
function setAttribute(this: HTMLElement, key: string, value: any) {
switch (value) {
case false:
case undefined:
removeAttribute.call(this, key);
break;
case true:
elemSetAttribute.call(this, key, "");
break;
default:
elemSetAttribute.call(this, key, value);
}
}
export function createAttrUpdater(attr: string): Setter<HTMLElement> { export function createAttrUpdater(attr: string): Setter<HTMLElement> {
return function (this: HTMLElement, value: any) { return function (this: HTMLElement, value: any) {
setAttribute.call(this, attr, value); if (value !== false) {
setAttribute.call(this, attr, value === true ? "" : value);
}
}; };
} }
@@ -156,10 +144,10 @@ export function isProp(tag: string, key: string): boolean {
case "option": case "option":
return key === "selected" || key === "disabled"; return key === "selected" || key === "disabled";
case "textarea": case "textarea":
return key === "value" || key === "readonly" || key === "disabled"; return key === "readonly" || key === "disabled";
case "select": break;
return key === "value" || key === "disabled";
case "button": case "button":
case "select":
case "optgroup": case "optgroup":
return key === "disabled"; return key === "disabled";
} }
+41 -52
View File
@@ -47,7 +47,7 @@ const cache: { [key: string]: BlockType } = {};
* @param str * @param str
* @returns a new block type, that can build concrete blocks * @returns a new block type, that can build concrete blocks
*/ */
export function createBlock(str: string, deepRemove: boolean = false): BlockType { export function createBlock(str: string): BlockType {
if (str in cache) { if (str in cache) {
return cache[str]; return cache[str];
} }
@@ -67,7 +67,7 @@ export function createBlock(str: string, deepRemove: boolean = false): BlockType
// step 3: build the final block class // step 3: build the final block class
const template = tree.el as HTMLElement; const template = tree.el as HTMLElement;
const Block = buildBlock(template, context, deepRemove); const Block = buildBlock(template, context);
cache[str] = Block; cache[str] = Block;
return Block; return Block;
} }
@@ -113,10 +113,9 @@ interface IntermediateTree {
nextSibling: IntermediateTree | null; nextSibling: IntermediateTree | null;
el: Node; el: Node;
info: DynamicInfo[]; info: DynamicInfo[];
isRef?: boolean; forceRef?: boolean;
refIdx?: number; refIdx?: number;
refN: number; refN: number;
currentNS: string | null;
} }
function buildTree( function buildTree(
@@ -125,9 +124,9 @@ function buildTree(
domParentTree: IntermediateTree | null = null domParentTree: IntermediateTree | null = null
): IntermediateTree { ): IntermediateTree {
switch (node.nodeType) { switch (node.nodeType) {
case Node.ELEMENT_NODE: { case 1: {
// HTMLElement // HTMLElement
let currentNS = domParentTree && domParentTree.currentNS; let isActive = false;
const tagName = (node as Element).tagName; const tagName = (node as Element).tagName;
let el: Node | undefined = undefined; let el: Node | undefined = undefined;
const info: DynamicInfo[] = []; const info: DynamicInfo[] = [];
@@ -135,31 +134,25 @@ function buildTree(
const index = parseInt(tagName.slice(11), 10); const index = parseInt(tagName.slice(11), 10);
info.push({ type: "text", idx: index }); info.push({ type: "text", idx: index });
el = document.createTextNode(""); el = document.createTextNode("");
isActive = true;
} }
if (tagName.startsWith("block-child-")) { if (tagName.startsWith("block-child-")) {
if (!domParentTree!.isRef) { domParentTree!.forceRef = true;
addRef(domParentTree!);
}
const index = parseInt(tagName.slice(12), 10); const index = parseInt(tagName.slice(12), 10);
info.push({ type: "child", idx: index }); info.push({ type: "child", idx: index });
el = document.createTextNode(""); el = document.createTextNode("");
} isActive = true;
const attrs = (node as Element).attributes;
const ns = attrs.getNamedItem("block-ns");
if (ns) {
attrs.removeNamedItem("block-ns");
currentNS = ns.value;
} }
if (!el) { if (!el) {
el = currentNS el = document.createElement(tagName);
? document.createElementNS(currentNS, tagName)
: document.createElement(tagName);
} }
if (el instanceof Element) { if (el instanceof HTMLElement) {
const attrs = (node as Element).attributes;
for (let i = 0; i < attrs.length; i++) { for (let i = 0; i < attrs.length; i++) {
const attrName = attrs[i].name; const attrName = attrs[i].name;
const attrValue = attrs[i].value; const attrValue = attrs[i].value;
if (attrName.startsWith("block-handler-")) { if (attrName.startsWith("block-handler-")) {
isActive = true;
const idx = parseInt(attrName.slice(14), 10); const idx = parseInt(attrName.slice(14), 10);
info.push({ info.push({
type: "handler", type: "handler",
@@ -167,6 +160,7 @@ function buildTree(
event: attrValue, event: attrValue,
}); });
} else if (attrName.startsWith("block-attribute-")) { } else if (attrName.startsWith("block-attribute-")) {
isActive = true;
const idx = parseInt(attrName.slice(16), 10); const idx = parseInt(attrName.slice(16), 10);
info.push({ info.push({
type: "attribute", type: "attribute",
@@ -175,11 +169,13 @@ function buildTree(
tag: tagName, tag: tagName,
}); });
} else if (attrName === "block-attributes") { } else if (attrName === "block-attributes") {
isActive = true;
info.push({ info.push({
type: "attributes", type: "attributes",
idx: parseInt(attrValue, 10), idx: parseInt(attrValue, 10),
}); });
} else if (attrName === "block-ref") { } else if (attrName === "block-ref") {
isActive = true;
info.push({ info.push({
type: "ref", type: "ref",
idx: parseInt(attrValue, 10), idx: parseInt(attrValue, 10),
@@ -196,20 +192,21 @@ function buildTree(
nextSibling: null, nextSibling: null,
el, el,
info, info,
refN: 0, refN: isActive ? 1 : 0,
currentNS,
}; };
if (node.firstChild) { if (node.firstChild) {
const childNode = node.childNodes[0]; const childNode = node.childNodes[0];
if ( if (
node.childNodes.length === 1 && node.childNodes.length === 1 &&
childNode.nodeType === Node.ELEMENT_NODE && childNode.nodeType === 1 &&
(childNode as Element).tagName.startsWith("block-child-") (childNode as Element).tagName.startsWith("block-child-")
) { ) {
const tagName = (childNode as Element).tagName; const tagName = (childNode as Element).tagName;
const index = parseInt(tagName.slice(12), 10); const index = parseInt(tagName.slice(12), 10);
info.push({ idx: index, type: "child", isOnlyChild: true }); info.push({ idx: index, type: "child", isOnlyChild: true });
isActive = true;
tree.refN = 1;
} else { } else {
tree.firstChild = buildTree(node.firstChild, tree, tree); tree.firstChild = buildTree(node.firstChild, tree, tree);
el.appendChild(tree.firstChild.el); el.appendChild(tree.firstChild.el);
@@ -222,16 +219,19 @@ function buildTree(
} }
} }
} }
if (tree.info.length) { if (isActive) {
addRef(tree); let cur: IntermediateTree | null = tree;
while ((cur = cur.parent)) {
cur.refN++;
}
} }
return tree; return tree;
} }
case Node.TEXT_NODE: case 3:
case Node.COMMENT_NODE: { case 8: {
// text node or comment node // text node or comment node
const el = const el =
node.nodeType === Node.TEXT_NODE node.nodeType === 3
? document.createTextNode(node.textContent!) ? document.createTextNode(node.textContent!)
: document.createComment(node.textContent!); : document.createComment(node.textContent!);
return { return {
@@ -241,20 +241,12 @@ function buildTree(
el, el,
info: [], info: [],
refN: 0, refN: 0,
currentNS: null,
}; };
} }
} }
throw new Error("boom"); throw new Error("boom");
} }
function addRef(tree: IntermediateTree) {
tree.isRef = true;
do {
tree.refN++;
} while ((tree = tree.parent as any));
}
function parentTree(tree: IntermediateTree): IntermediateTree | null { function parentTree(tree: IntermediateTree): IntermediateTree | null {
let parent = tree.parent; let parent = tree.parent;
while (parent && parent.nextSibling === tree) { while (parent && parent.nextSibling === tree) {
@@ -301,15 +293,21 @@ interface BlockCtx {
cbRefs: number[]; cbRefs: number[];
} }
function buildContext(tree: IntermediateTree, ctx?: BlockCtx, fromIdx?: number): BlockCtx { function buildContext(
tree: IntermediateTree,
ctx?: BlockCtx,
fromIdx?: number,
toIdx?: number
): BlockCtx {
if (!ctx) { if (!ctx) {
const children = new Array(tree.info.filter((v) => v.type === "child").length); const children = new Array(tree.info.filter((v) => v.type === "child").length);
ctx = { collectors: [], locations: [], children, cbRefs: [], refN: tree.refN }; ctx = { collectors: [], locations: [], children, cbRefs: [], refN: tree.refN };
fromIdx = 0; fromIdx = 0;
toIdx = tree.refN - 1;
} }
if (tree.refN) { if (tree.refN) {
const initialIdx = fromIdx!; const initialIdx = fromIdx!;
const isRef = tree.isRef; const isRef = tree.forceRef || tree.info.length > 0;
const firstChild = tree.firstChild ? tree.firstChild.refN : 0; const firstChild = tree.firstChild ? tree.firstChild.refN : 0;
const nextSibling = tree.nextSibling ? tree.nextSibling.refN : 0; const nextSibling = tree.nextSibling ? tree.nextSibling.refN : 0;
@@ -327,13 +325,13 @@ function buildContext(tree: IntermediateTree, ctx?: BlockCtx, fromIdx?: number):
if (nextSibling) { if (nextSibling) {
const idx = fromIdx! + firstChild; const idx = fromIdx! + firstChild;
ctx.collectors.push({ idx, prevIdx: initialIdx, getVal: nodeGetNextSibling }); ctx.collectors.push({ idx, prevIdx: initialIdx, getVal: nodeGetNextSibling });
buildContext(tree.nextSibling!, ctx, idx); buildContext(tree.nextSibling!, ctx, idx, toIdx);
} }
// left // left
if (firstChild) { if (firstChild) {
ctx.collectors.push({ idx: fromIdx!, prevIdx: initialIdx, getVal: nodeGetFirstChild }); ctx.collectors.push({ idx: fromIdx!, prevIdx: initialIdx, getVal: nodeGetFirstChild });
buildContext(tree.firstChild!, ctx, fromIdx!); buildContext(tree.firstChild!, ctx, fromIdx!, toIdx! - nextSibling);
} }
} }
@@ -398,12 +396,12 @@ function updateCtx(ctx: BlockCtx, tree: IntermediateTree) {
}); });
break; break;
case "handler": { case "handler": {
const { setup, update } = createEventHandler(info.event!); const setupHandler = createEventHandler(info.event!);
ctx.locations.push({ ctx.locations.push({
idx: info.idx, idx: info.idx,
refIdx: info.refIdx!, refIdx: info.refIdx!,
setData: setup, setData: setupHandler,
updateData: update, updateData: setupHandler,
}); });
break; break;
} }
@@ -422,7 +420,7 @@ function updateCtx(ctx: BlockCtx, tree: IntermediateTree) {
// building the concrete block class // building the concrete block class
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
function buildBlock(template: HTMLElement, ctx: BlockCtx, deepRemove: boolean): BlockType { function buildBlock(template: HTMLElement, ctx: BlockCtx): BlockType {
let B = createBlockClass(template, ctx); let B = createBlockClass(template, ctx);
if (ctx.cbRefs.length) { if (ctx.cbRefs.length) {
@@ -447,14 +445,6 @@ function buildBlock(template: HTMLElement, ctx: BlockCtx, deepRemove: boolean):
} }
}; };
B.prototype.beforeRemove = VMulti.prototype.beforeRemove; B.prototype.beforeRemove = VMulti.prototype.beforeRemove;
if (deepRemove) {
const blockRemove = B.prototype.remove;
const vMultiRemove = VMulti.prototype.remove;
B.prototype.remove = function () {
blockRemove.call(this);
vMultiRemove.call(this);
};
}
return (data?: any[], children: (VNode | undefined)[] = []) => new B(data, children); return (data?: any[], children: (VNode | undefined)[] = []) => new B(data, children);
} }
@@ -498,7 +488,6 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
beforeRemove() {} beforeRemove() {}
remove() { remove() {
console.log('ddd')
elementRemove.call(this.el); elementRemove.call(this.el);
} }
+1 -13
View File
@@ -1,13 +1,3 @@
export function filterOutModifiersFromData(dataList: any[]): { modifiers: string[]; data: any[] } {
dataList = dataList.slice();
const modifiers = [];
let elm;
while ((elm = dataList[0]) && typeof elm === "string") {
modifiers.push(dataList.shift());
}
return { modifiers, data: dataList };
}
export const config = { export const config = {
// whether or not blockdom should normalize DOM whenever a block is created. // whether or not blockdom should normalize DOM whenever a block is created.
// Normalizing dom mean removing empty text nodes (or containing only spaces) // Normalizing dom mean removing empty text nodes (or containing only spaces)
@@ -16,13 +6,11 @@ export const config = {
// this is the main event handler. Every event handler registered with blockdom // this is the main event handler. Every event handler registered with blockdom
// will go through this function, giving it the data registered in the block // will go through this function, giving it the data registered in the block
// and the event // and the event
mainEventHandler: (data: any, ev: Event, currentTarget?: EventTarget | null): boolean => { mainEventHandler: (data: any, ev: Event) => {
if (typeof data === "function") { if (typeof data === "function") {
data(ev); data(ev);
} else if (Array.isArray(data)) { } else if (Array.isArray(data)) {
data = filterOutModifiersFromData(data).data;
data[0](data[1], ev); data[0](data[1], ev);
} }
return false;
}, },
}; };
+16 -74
View File
@@ -1,77 +1,21 @@
import { config } from "./config"; import { config } from "./config";
type EventHandlerSetter = (this: HTMLElement, data: any) => void; export function createEventHandler(event: string) {
setupSyntheticEvent(event);
interface EventHandlerCreator { const key = `__event__${event}`;
setup: EventHandlerSetter; return function setupHandler(this: HTMLElement, data: any) {
update: EventHandlerSetter; (this as any)[key] = data;
};
} }
export function createEventHandler(rawEvent: string): EventHandlerCreator { function nativeToSyntheticEvent(event: Event, name: string) {
const eventName = rawEvent.split(".")[0]; const eventKey = `__event__${name}`;
const capture = rawEvent.includes(".capture");
if (rawEvent.includes(".synthetic")) {
return createSyntheticHandler(eventName, capture);
} else {
return createElementHandler(eventName, capture);
}
}
// Native listener
let nextNativeEventId = 1;
function createElementHandler(evName: string, capture: boolean = false): EventHandlerCreator {
let eventKey = `__event__${evName}_${nextNativeEventId++}`;
if (capture) {
eventKey = `${eventKey}_capture`;
}
function listener(ev: Event) {
const currentTarget = ev.currentTarget;
if (!currentTarget || !document.contains(currentTarget as HTMLElement)) return;
const data = (currentTarget as any)[eventKey];
if (!data) return;
config.mainEventHandler(data, ev, currentTarget);
}
function setup(this: HTMLElement, data: any) {
(this as any)[eventKey] = data;
this.addEventListener(evName, listener, { capture });
}
function update(this: HTMLElement, data: any) {
(this as any)[eventKey] = data;
}
return { setup, update };
}
// Synthetic handler: a form of event delegation that allows placing only one
// listener per event type.
let nextSyntheticEventId = 1;
function createSyntheticHandler(evName: string, capture: boolean = false): EventHandlerCreator {
let eventKey = `__event__synthetic_${evName}`;
if (capture) {
eventKey = `${eventKey}_capture`;
}
setupSyntheticEvent(evName, eventKey, capture);
const currentId = nextSyntheticEventId++;
function setup(this: HTMLElement, data: any) {
const _data = (this as any)[eventKey] || {};
_data[currentId] = data;
(this as any)[eventKey] = _data;
}
return { setup, update: setup };
}
function nativeToSyntheticEvent(eventKey: string, event: Event) {
let dom = event.target; let dom = event.target;
while (dom !== null) { while (dom !== null) {
const _data = (dom as any)[eventKey]; const data = (dom as any)[eventKey];
if (_data) { if (data) {
for (const data of Object.values(_data)) { config.mainEventHandler(data, event);
const stopped = config.mainEventHandler(data, event, dom); return;
if (stopped) return;
}
} }
dom = (dom as any).parentNode; dom = (dom as any).parentNode;
} }
@@ -79,12 +23,10 @@ function nativeToSyntheticEvent(eventKey: string, event: Event) {
const CONFIGURED_SYNTHETIC_EVENTS: { [event: string]: boolean } = {}; const CONFIGURED_SYNTHETIC_EVENTS: { [event: string]: boolean } = {};
function setupSyntheticEvent(evName: string, eventKey: string, capture: boolean = false) { function setupSyntheticEvent(name: string) {
if (CONFIGURED_SYNTHETIC_EVENTS[eventKey]) { if (CONFIGURED_SYNTHETIC_EVENTS[name]) {
return; return;
} }
document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event), { document.addEventListener(name, (event) => nativeToSyntheticEvent(event, name));
capture, CONFIGURED_SYNTHETIC_EVENTS[name] = true;
});
CONFIGURED_SYNTHETIC_EVENTS[eventKey] = true;
} }
+3 -3
View File
@@ -4,7 +4,7 @@ export { toggler } from "./toggler";
export { createBlock } from "./block_compiler"; export { createBlock } from "./block_compiler";
export { list } from "./list"; export { list } from "./list";
export { multi } from "./multi"; export { multi } from "./multi";
export { text, comment } from "./text"; export { text } from "./text";
export { html } from "./html"; export { html } from "./html";
export interface VNode<T = any> { export interface VNode<T = any> {
@@ -23,8 +23,8 @@ export interface VNode<T = any> {
export type BDom = VNode<any>; export type BDom = VNode<any>;
export function mount(vnode: VNode, fixture: HTMLElement, afterNode: Node | null = null) { export function mount(vnode: VNode, fixture: HTMLElement) {
vnode.mount(fixture, afterNode); vnode.mount(fixture, null);
} }
export function patch(vnode1: VNode, vnode2: VNode, withBeforeRemove: boolean = false) { export function patch(vnode1: VNode, vnode2: VNode, withBeforeRemove: boolean = false) {
+6 -7
View File
@@ -17,11 +17,9 @@ class VList {
anchor: Node | undefined; anchor: Node | undefined;
parentEl?: HTMLElement | undefined; parentEl?: HTMLElement | undefined;
isOnlyChild?: boolean | undefined; isOnlyChild?: boolean | undefined;
deepRemove: boolean;
constructor(children: VNode[], deepRemove: boolean) { constructor(children: VNode[]) {
this.children = children; this.children = children;
this.deepRemove = deepRemove;
} }
mount(parent: HTMLElement, afterNode: Node | null) { mount(parent: HTMLElement, afterNode: Node | null) {
@@ -77,7 +75,7 @@ class VList {
const parent = this.parentEl!; const parent = this.parentEl!;
// fast path: no new child => only remove // fast path: no new child => only remove
if (ch2.length === 0 && isOnlyChild && !this.deepRemove) { if (ch2.length === 0 && isOnlyChild) {
if (withBeforeRemove) { if (withBeforeRemove) {
for (let i = 0, l = ch1.length; i < l; i++) { for (let i = 0, l = ch1.length; i < l; i++) {
beforeRemove.call(ch1[i]); beforeRemove.call(ch1[i]);
@@ -100,6 +98,7 @@ class VList {
let endVn2 = ch2[endIdx2]; let endVn2 = ch2[endIdx2];
let mapping: any = undefined; let mapping: any = undefined;
// let noFullRemove = this.hasNoComponent;
while (startIdx1 <= endIdx1 && startIdx2 <= endIdx2) { while (startIdx1 <= endIdx1 && startIdx2 <= endIdx2) {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -203,7 +202,7 @@ class VList {
remove() { remove() {
const { parentEl, anchor } = this; const { parentEl, anchor } = this;
if (this.isOnlyChild && !this.deepRemove) { if (this.isOnlyChild) {
nodeSetTextContent.call(parentEl, ""); nodeSetTextContent.call(parentEl, "");
} else { } else {
const children = this.children; const children = this.children;
@@ -228,8 +227,8 @@ class VList {
} }
} }
export function list(children: VNode[], deepRemove = false): VNode<VList> { export function list(children: VNode[]): VNode<VList> {
return new VList(children, deepRemove); return new VList(children);
} }
function createMapping(ch1: any[], startIdx1: number, endIdx2: number): { [key: string]: any } { function createMapping(ch1: any[], startIdx1: number, endIdx2: number): { [key: string]: any } {
+4 -6
View File
@@ -15,11 +15,9 @@ export class VMulti {
anchors?: Node[] | undefined; anchors?: Node[] | undefined;
parentEl?: HTMLElement | undefined; parentEl?: HTMLElement | undefined;
isOnlyChild?: boolean | undefined; isOnlyChild?: boolean | undefined;
deepRemove: boolean;
constructor(children: (VNode | undefined)[], deepRemove: boolean) { constructor(children: (VNode | undefined)[]) {
this.children = children; this.children = children;
this.deepRemove = deepRemove;
} }
mount(parent: HTMLElement, afterNode: Node | null) { mount(parent: HTMLElement, afterNode: Node | null) {
@@ -105,7 +103,7 @@ export class VMulti {
remove() { remove() {
const parentEl = this.parentEl; const parentEl = this.parentEl;
if (this.isOnlyChild && !this.deepRemove) { if (this.isOnlyChild) {
nodeSetTextContent.call(parentEl, ""); nodeSetTextContent.call(parentEl, "");
} else { } else {
const children = this.children; const children = this.children;
@@ -131,6 +129,6 @@ export class VMulti {
} }
} }
export function multi(children: (VNode | undefined)[], deepRemove = false): VNode<VMulti> { export function multi(children: (VNode | undefined)[]): VNode<VMulti> {
return new VMulti(children, deepRemove); return new VMulti(children);
} }
+12 -29
View File
@@ -8,17 +8,18 @@ const nodeInsertBefore = nodeProto.insertBefore;
const characterDataSetData = getDescriptor(characterDataProto, "data").set!; const characterDataSetData = getDescriptor(characterDataProto, "data").set!;
const nodeRemoveChild = nodeProto.removeChild; const nodeRemoveChild = nodeProto.removeChild;
abstract class VSimpleNode { class VText {
text: string; text: string;
parentEl?: HTMLElement | undefined; parentEl?: HTMLElement | undefined;
el?: any; el?: Text;
constructor(text: string) { constructor(text: string) {
this.text = text; this.text = text;
} }
mountNode(node: Node, parent: HTMLElement, afterNode: Node | null) { mount(parent: HTMLElement, afterNode: Node | null) {
this.parentEl = parent; this.parentEl = parent;
const node = document.createTextNode(toText(this.text));
nodeInsertBefore.call(parent, node, afterNode); nodeInsertBefore.call(parent, node, afterNode);
this.el = node; this.el = node;
} }
@@ -28,6 +29,14 @@ abstract class VSimpleNode {
nodeInsertBefore.call(this.parentEl, this.el!, target); nodeInsertBefore.call(this.parentEl, this.el!, target);
} }
patch(other: VText) {
const text2 = other.text;
if (this.text !== text2) {
characterDataSetData.call(this.el!, toText(text2));
this.text = text2;
}
}
beforeRemove() {} beforeRemove() {}
remove() { remove() {
@@ -43,36 +52,10 @@ abstract class VSimpleNode {
} }
} }
class VText extends VSimpleNode {
mount(parent: HTMLElement, afterNode: Node | null) {
this.mountNode(document.createTextNode(toText(this.text)), parent, afterNode);
}
patch(other: VText) {
const text2 = other.text;
if (this.text !== text2) {
characterDataSetData.call(this.el!, toText(text2));
this.text = text2;
}
}
}
class VComment extends VSimpleNode {
mount(parent: HTMLElement, afterNode: Node | null) {
this.mountNode(document.createComment(toText(this.text)), parent, afterNode);
}
patch() {}
}
export function text(str: string): VNode<VText> { export function text(str: string): VNode<VText> {
return new VText(str); return new VText(str);
} }
export function comment(str: string): VNode<VComment> {
return new VComment(str);
}
export function toText(value: any): string { export function toText(value: any): string {
switch (typeof value) { switch (typeof value) {
case "string": case "string":
+1 -3
View File
@@ -43,9 +43,7 @@ class VToggler {
} }
} }
beforeRemove() { beforeRemove() {}
this.child.beforeRemove();
}
remove() { remove() {
this.child.remove(); this.child.remove();
-27
View File
@@ -1,27 +0,0 @@
import type { BDom } from "../blockdom";
import { CodeGenerator, Config } from "./code_generator";
import { parse } from "./parser";
export type Template = (context: any, vnode: any, key?: string) => BDom;
export type TemplateFunction = (blocks: any, utils: any) => Template;
interface CompileOptions extends Config {
name?: string;
}
export function compile(template: string | Node, options: CompileOptions = {}): TemplateFunction {
// parsing
const ast = parse(template);
// some work
const hasSafeContext =
template instanceof Node
? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
: !template.includes("t-set") && !template.includes("t-call");
// code generation
const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
const code = codeGenerator.generateCode();
// template function
return new Function("bdom, helpers", code) as TemplateFunction;
}
+8 -6
View File
@@ -1,4 +1,3 @@
import type { Env } from "../app/app";
import type { ComponentNode } from "./component_node"; import type { ComponentNode } from "./component_node";
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -7,21 +6,24 @@ import type { ComponentNode } from "./component_node";
export class Component { export class Component {
static template: string = ""; static template: string = "";
static props?: any;
props: any; props: any;
env: Env; env: any;
__owl__: ComponentNode; __owl__: ComponentNode;
constructor(props: any, env: Env, node: ComponentNode) { constructor(props: any, env: any, node: ComponentNode) {
this.props = props; this.props = props;
this.env = env; this.env = env;
this.__owl__ = node; this.__owl__ = node;
} }
get el(): HTMLElement | Text | undefined {
const node = this.__owl__;
return node.bdom ? (node.bdom.firstNode() as any) : undefined;
}
setup() {} setup() {}
render() { render(): Promise<void> {
this.__owl__.render(); return this.__owl__.render();
} }
} }
+93 -147
View File
@@ -1,4 +1,4 @@
import type { App, Env } from "../app/app"; import type { App } from "../app";
import { BDom, VNode } from "../blockdom"; import { BDom, VNode } from "../blockdom";
import { Component } from "./component"; import { Component } from "./component";
import { import {
@@ -6,23 +6,11 @@ import {
makeChildFiber, makeChildFiber,
makeRootFiber, makeRootFiber,
MountFiber, MountFiber,
MountOptions,
RootFiber, RootFiber,
__internal__destroyed,
} from "./fibers"; } from "./fibers";
import { handleError, fibersInError } from "./error_handling";
import { applyDefaultProps } from "./props_validation";
import { STATUS } from "./status"; import { STATUS } from "./status";
let currentNode: ComponentNode | null = null;
export function getCurrent(): ComponentNode | null {
return currentNode;
}
export function useComponent(): Component {
return currentNode!.component;
}
export function component( export function component(
name: string | typeof Component, name: string | typeof Component,
props: any, props: any,
@@ -33,13 +21,9 @@ export function component(
let node: any = ctx.children[key]; let node: any = ctx.children[key];
let isDynamic = typeof name !== "string"; let isDynamic = typeof name !== "string";
if (node) { if (node && node.status < STATUS.MOUNTED) {
if (node.status < STATUS.MOUNTED) { node.destroy();
node.destroy(); node = undefined;
node = undefined;
} else if (node.status === STATUS.DESTROYED) {
node = undefined;
}
} }
if (isDynamic && node && node.component.constructor !== name) { if (isDynamic && node && node.component.constructor !== name) {
node = undefined; node = undefined;
@@ -50,16 +34,8 @@ export function component(
node.updateAndRender(props, parentFiber); node.updateAndRender(props, parentFiber);
} else { } else {
// new component // new component
let C; const C = isDynamic ? name : parent.constructor.components[name as any];
if (isDynamic) { node = new ComponentNode(C, props, ctx.app);
C = name;
} else {
C = parent.constructor.components[name as any];
if (!C) {
throw new Error(`Cannot find the definition of component "${name}"`);
}
}
node = new ComponentNode(C, props, ctx.app, ctx);
ctx.children[key] = node; ctx.children[key] = node;
const fiber = makeChildFiber(node, parentFiber); const fiber = makeChildFiber(node, parentFiber);
@@ -72,12 +48,17 @@ export function component(
// Component VNode // Component VNode
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
let currentNode: ComponentNode | null = null;
export function getCurrent(): ComponentNode | null {
return currentNode;
}
type LifecycleHook = Function; type LifecycleHook = Function;
export class ComponentNode<T extends typeof Component = typeof Component> export class ComponentNode<T extends typeof Component = any> implements VNode<ComponentNode> {
implements VNode<ComponentNode>
{
el?: HTMLElement | Text | undefined; el?: HTMLElement | Text | undefined;
handlers: any = null;
app: App; app: App;
fiber: Fiber | null = null; fiber: Fiber | null = null;
component: InstanceType<T>; component: InstanceType<T>;
@@ -85,10 +66,8 @@ export class ComponentNode<T extends typeof Component = typeof Component>
status: STATUS = STATUS.NEW; status: STATUS = STATUS.NEW;
renderFn: Function; renderFn: Function;
parent: ComponentNode | null;
level: number;
childEnv: Env;
children: { [key: string]: ComponentNode } = Object.create(null); children: { [key: string]: ComponentNode } = Object.create(null);
slots: any = {};
refs: any = {}; refs: any = {};
willStart: LifecycleHook[] = []; willStart: LifecycleHook[] = [];
@@ -97,159 +76,118 @@ export class ComponentNode<T extends typeof Component = typeof Component>
mounted: LifecycleHook[] = []; mounted: LifecycleHook[] = [];
willPatch: LifecycleHook[] = []; willPatch: LifecycleHook[] = [];
patched: LifecycleHook[] = []; patched: LifecycleHook[] = [];
willDestroy: LifecycleHook[] = []; destroyed: LifecycleHook[] = [];
constructor(C: T, props: any, app: App, parent?: ComponentNode) { constructor(C: T, props: any, app: App) {
currentNode = this; currentNode = this;
this.app = app; this.app = app;
this.parent = parent || null; this.component = new C(props, app.env, this) as any;
this.level = parent ? parent.level + 1 : 0; this.renderFn = app.getTemplate(C.template).bind(null, this.component, this);
applyDefaultProps(props, C);
const env = (parent && parent.childEnv) || app.env;
this.childEnv = env;
this.component = new C(props, env, this) as any;
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
this.component.setup(); this.component.setup();
} }
mountComponent(target: any, options?: MountOptions) { mountComponent(target: any): Promise<InstanceType<T>> {
const fiber = new MountFiber(this, target, options); const fiber = new MountFiber(this, target);
this.app.scheduler.addFiber(fiber); this.app.scheduler.addFiber(fiber);
this.initiateRender(fiber); this.initiateRender(fiber);
return fiber.promise.then(() => this.component);
} }
async initiateRender(fiber: Fiber | MountFiber) { async initiateRender(fiber: Fiber | MountFiber) {
this.fiber = fiber;
if (this.mounted.length) { if (this.mounted.length) {
fiber.root.mounted.push(fiber); fiber.root.mounted.push(fiber);
} }
const component = this.component; const component = this.component;
try { const prom = Promise.all(this.willStart.map((f) => f.call(component)));
await Promise.all(this.willStart.map((f) => f.call(component))); await prom;
} catch (e) {
handleError({ node: this, error: e });
return;
}
if (this.status === STATUS.NEW && this.fiber === fiber) { if (this.status === STATUS.NEW && this.fiber === fiber) {
this._render(fiber); this._render(fiber);
} }
} }
async render() { async render() {
let current = this.fiber; if (this.fiber && !this.fiber.bdom) {
if (current && current.root.locked) { return this.fiber.root.promise;
await Promise.resolve();
// situation may have changed after the microtask tick
current = this.fiber;
} }
if (current && !current.bdom && !fibersInError.has(current)) { if (!this.bdom && !this.fiber) {
return; // should find a way to return the future mounting promise
}
if (!this.bdom && !current) {
return; return;
} }
const fiber = makeRootFiber(this); const fiber = makeRootFiber(this);
this.fiber = fiber;
this.app.scheduler.addFiber(fiber); this.app.scheduler.addFiber(fiber);
await Promise.resolve(); await Promise.resolve();
if (this.status === STATUS.DESTROYED) { if (this.status === STATUS.DESTROYED) {
return; return;
} }
// We only want to actually render the component if the following two if (this.fiber === fiber) {
// conditions are true:
// * this.fiber: it could be null, in which case the render has been cancelled
// * (current || !fiber.parent): if current is not null, this means that the
// render function was called when a render was already occurring. In this
// case, the pending rendering was cancelled, and the fiber needs to be
// rendered to complete the work. If current is null, we check that the
// fiber has no parent. If that is the case, the fiber was downgraded from
// a root fiber to a child fiber in the previous microtick, because it was
// embedded in a rendering coming from above, so the fiber will be rendered
// in the next microtick anyway, so we should not render it again.
if (this.fiber && (current || !fiber.parent)) {
this._render(fiber); this._render(fiber);
} }
return fiber.root.promise;
} }
_render(fiber: Fiber | RootFiber) { _render(fiber: Fiber | RootFiber) {
try { try {
fiber.bdom = this.renderFn(); fiber.bdom = this.renderFn();
fiber.root.counter--;
} catch (e) { } catch (e) {
handleError({ node: this, error: e }); fiber.root.error = e;
this.handleError(fiber);
} }
fiber.root.counter--;
}
handleError(fiber: Fiber) {
fiber.node.app.destroy();
} }
destroy() { destroy() {
let shouldRemove = this.status === STATUS.MOUNTED; if (this.status === STATUS.MOUNTED) {
this._destroy(); callWillUnmount(this);
if (shouldRemove) {
this.bdom!.remove(); this.bdom!.remove();
} }
} callDestroyed(this);
_destroy() { function callWillUnmount(node: ComponentNode) {
const component = this.component; const component = node.component;
if (this.status === STATUS.MOUNTED) { for (let cb of node.willUnmount) {
for (let cb of this.willUnmount) { cb.call(component);
}
for (let child of Object.values(node.children)) {
if (child.status === STATUS.MOUNTED) {
callWillUnmount(child);
}
}
}
function callDestroyed(node: ComponentNode) {
const component = node.component;
node.status = STATUS.DESTROYED;
for (let child of Object.values(node.children)) {
callDestroyed(child);
}
for (let cb of node.destroyed) {
cb.call(component); cb.call(component);
} }
} }
for (let child of Object.values(this.children)) {
child._destroy();
}
for (let cb of this.willDestroy) {
cb.call(component);
}
this.status = STATUS.DESTROYED;
} }
async updateAndRender(props: any, parentFiber: Fiber) { async updateAndRender(props: any, parentFiber: Fiber) {
// update // update
const fiber = makeChildFiber(this, parentFiber); const fiber = makeChildFiber(this, parentFiber);
this.fiber = fiber; if (this.willPatch.length) {
parentFiber.root.willPatch.push(fiber);
}
if (this.patched.length) {
parentFiber.root.patched.push(fiber);
}
const component = this.component; const component = this.component;
applyDefaultProps(props, component.constructor as any);
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props))); const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
await prom; await prom;
if (fiber !== this.fiber) { if (fiber !== this.fiber) {
return; return;
} }
component.props = props; this.component.props = props;
this._render(fiber); this._render(fiber);
const parentRoot = parentFiber.root;
if (this.willPatch.length) {
parentRoot.willPatch.push(fiber);
}
if (this.patched.length) {
parentRoot.patched.push(fiber);
}
}
/**
* Finds a child that has dom that is not yet updated, and update it. This
* method is meant to be used only in the context of repatching the dom after
* a mounted hook failed and was handled.
*/
updateDom() {
if (!this.fiber) {
return;
}
if (this.bdom === this.fiber!.bdom) {
// If the error was handled by some child component, we need to find it to
// apply its change
for (let k in this.children) {
const child = this.children[k];
child.updateDom();
}
} else {
// if we get here, this is the component that handled the error and rerendered
// itself, so we can simply patch the dom
this.bdom!.patch(this.fiber!.bdom, false);
this.fiber!.appliedToDom = true;
this.fiber = null;
}
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -268,6 +206,18 @@ export class ComponentNode<T extends typeof Component = typeof Component>
this.status = STATUS.MOUNTED; this.status = STATUS.MOUNTED;
this.fiber!.appliedToDom = true; this.fiber!.appliedToDom = true;
this.fiber = null; this.fiber = null;
if (this.handlers) {
for (let i = 0; i < this.handlers.length; i++) {
const handler = this.handlers[i];
const eventType = handler[0];
const el = bdom.el!;
el.addEventListener(eventType, (ev: Event) => {
const info = this.handlers![i];
const [, ctx, method] = info;
(ctx.__owl__.component as any)[method](ev);
});
}
}
} }
moveBefore(other: ComponentNode | null, afterNode: Node | null) { moveBefore(other: ComponentNode | null, afterNode: Node | null) {
@@ -276,35 +226,31 @@ export class ComponentNode<T extends typeof Component = typeof Component>
patch() { patch() {
this.bdom!.patch(this!.fiber!.bdom!, false); this.bdom!.patch(this!.fiber!.bdom!, false);
this.cleanOutdatedChildren();
this.fiber!.appliedToDom = true; this.fiber!.appliedToDom = true;
this.fiber = null; this.fiber = null;
} }
beforeRemove() { beforeRemove() {
console.log('ddddddd') visitRemovedNodes(this);
this._destroy();
} }
remove() { remove() {
console.log('coucou')
this.bdom!.remove(); this.bdom!.remove();
} }
}
cleanOutdatedChildren() { function visitRemovedNodes(node: ComponentNode) {
const childrenEntries = Object.entries(this.children); if (node.status === STATUS.MOUNTED) {
if (!childrenEntries.length) { const component = node.component;
return; for (let cb of node.willUnmount) {
} cb.call(component);
const children = this.children;
for (const [key, node] of childrenEntries) {
const status = node.status;
if (status !== STATUS.MOUNTED) {
delete children[key];
if (status !== STATUS.DESTROYED) {
node.destroy();
}
}
} }
} }
for (let child of Object.values(node.children)) {
visitRemovedNodes(child);
}
node.status = STATUS.DESTROYED;
if (node.destroyed.length) {
__internal__destroyed.push(node);
}
} }
-66
View File
@@ -1,66 +0,0 @@
import type { ComponentNode } from "./component_node";
import type { Fiber } from "./fibers";
// Maps fibers to thrown errors
export const fibersInError: WeakMap<Fiber, any> = new WeakMap();
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap();
function _handleError(node: ComponentNode | null, error: any, isFirstRound = false): boolean {
if (!node) {
return false;
}
const fiber = node.fiber;
if (fiber) {
fibersInError.set(fiber, error);
}
const errorHandlers = nodeErrorHandlers.get(node);
if (errorHandlers) {
let stopped = false;
// execute in the opposite order
for (let i = errorHandlers.length - 1; i >= 0; i--) {
try {
errorHandlers[i](error);
stopped = true;
break;
} catch (e) {
error = e;
}
}
if (stopped) {
if (isFirstRound && fiber) {
fiber.root.counter--;
}
return true;
}
}
return _handleError(node.parent, error);
}
type ErrorParams = { error: any } & ({ node: ComponentNode } | { fiber: Fiber });
export function handleError(params: ErrorParams) {
const error = params.error;
const node = "node" in params ? params.node : params.fiber.node;
const fiber = "fiber" in params ? params.fiber : node.fiber!;
// resets the fibers on components if possible. This is important so that
// new renderings can be properly included in the initial one, if any.
let current: Fiber | null = fiber;
do {
current.node.fiber = current;
current = current.parent;
} while (current);
fibersInError.set(fiber.root, error);
const handled = _handleError(node, error, true);
if (!handled) {
console.warn(`[Owl] Unhandled error. Destroying the root component`);
try {
node.app.destroy();
} catch (e) {
console.error(e);
}
}
}
+82 -133
View File
@@ -1,46 +1,18 @@
import { BDom, mount } from "../blockdom"; import type { BDom } from "../blockdom";
import { mount } from "../blockdom";
import type { ComponentNode } from "./component_node"; import type { ComponentNode } from "./component_node";
import { fibersInError, handleError } from "./error_handling";
import { STATUS } from "./status"; import { STATUS } from "./status";
// import { mountBlock } from "./bdom/block";
/**
* Cleans on the root fiber the patch and willPatch fiber lists
* It is typically needed when the same root fiber needs to recycle on
* of its children or grandchildren's fiber.
*/
function cleanPatchableFiber(child: Fiber, root: RootFiber) {
const { willPatch, patched } = root;
let i = willPatch.indexOf(child);
if (i > -1) {
willPatch.splice(i, 1);
}
i = patched.indexOf(child);
if (i > -1) {
patched.splice(i, 1);
}
}
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber { export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
let current = node.fiber; let current = node.fiber;
if (current) { if (current) {
// current is necessarily a rootfiber here // current is necessarily a rootfiber here
let root = parent.root; let root = parent.root;
const isSameRoot = current.root === root;
cancelFibers(root, current.children); cancelFibers(root, current.children);
current.children = []; current.children = [];
current.parent = parent; current.parent = parent;
// only increment our rendering if we were not root.counter++;
// already accounted for, or that we have been rendered
// already (in which case our fiber was removed from the root rendering)
if (!isSameRoot || current.bdom) {
root.counter++;
}
if (isSameRoot) {
cleanPatchableFiber(current, root);
}
current.bdom = null;
current.root = root; current.root = root;
return current; return current;
} }
@@ -55,14 +27,9 @@ export function makeRootFiber(node: ComponentNode): Fiber {
current.children = []; current.children = [];
root.counter++; root.counter++;
current.bdom = null; current.bdom = null;
if (fibersInError.has(current)) {
fibersInError.delete(current);
fibersInError.delete(root);
current.appliedToDom = false;
}
return current; return current;
} }
const fiber = new RootFiber(node, null); const fiber = new RootFiber(node);
if (node.willPatch.length) { if (node.willPatch.length) {
fiber.willPatch.push(fiber); fiber.willPatch.push(fiber);
} }
@@ -99,6 +66,7 @@ export class Fiber {
constructor(node: ComponentNode, parent: Fiber | null) { constructor(node: ComponentNode, parent: Fiber | null) {
this.node = node; this.node = node;
node.fiber = this;
this.parent = parent; this.parent = parent;
if (parent) { if (parent) {
const root = parent.root; const root = parent.root;
@@ -113,124 +81,105 @@ export class Fiber {
export class RootFiber extends Fiber { export class RootFiber extends Fiber {
counter: number = 1; counter: number = 1;
error: Error | null = null;
resolve: any;
promise: Promise<any>;
reject: any;
// only add stuff in this if they have registered some hooks // only add stuff in this if they have registered some hooks
willPatch: Fiber[] = []; willPatch: Fiber[] = [];
patched: Fiber[] = []; patched: Fiber[] = [];
mounted: Fiber[] = []; mounted: Fiber[] = [];
// A fiber is typically locked when it is completing and the patch has not, or is being applied.
// i.e.: render triggered in onWillUnmount or in willPatch will be delayed constructor(node: ComponentNode) {
locked: boolean = false; super(node, null);
this.counter = 1;
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
complete() { complete() {
const node = this.node; const node = this.node;
this.locked = true;
let current: Fiber | undefined = undefined; // Step 1: calling all willPatch lifecycle hooks
try { for (let fiber of this.willPatch) {
// Step 1: calling all willPatch lifecycle hooks // because of the asynchronous nature of the rendering, some parts of the
for (current of this.willPatch) { // UI may have been rendered, then deleted in a followup rendering, and we
// because of the asynchronous nature of the rendering, some parts of the // do not want to call onWillPatch in that case.
// UI may have been rendered, then deleted in a followup rendering, and we let node = fiber.node;
// do not want to call onWillPatch in that case. if (node.fiber === fiber) {
let node = current.node; const component = node.component;
if (node.fiber === current) { for (let cb of node.willPatch) {
const component = node.component; cb.call(component);
for (let cb of node.willPatch) {
cb.call(component);
}
} }
} }
current = undefined;
// Step 2: patching the dom
node.bdom!.patch(this.bdom!, Object.keys(node.children).length > 0);
node.cleanOutdatedChildren();
this.appliedToDom = true;
this.locked = false;
// unregistering the fiber before mounted since it can do another render
// and that the current rendering is obviously completed
node.fiber = null;
// Step 4: calling all mounted lifecycle hooks
let mountedFibers = this.mounted;
while ((current = mountedFibers.pop())) {
current = current;
if (current.appliedToDom) {
for (let cb of current.node.mounted) {
cb();
}
}
}
// Step 5: calling all patched hooks
let patchedFibers = this.patched;
while ((current = patchedFibers.pop())) {
current = current;
if (current.appliedToDom) {
for (let cb of current.node.patched) {
cb();
}
}
}
} catch (e) {
this.locked = false;
handleError({ fiber: current || this, error: e });
} }
// Step 2: patching the dom
node.bdom!.patch(this.bdom!, Object.keys(node.children).length > 0);
this.appliedToDom = true;
// Step 3: calling all destroyed hooks
for (let node of __internal__destroyed) {
for (let cb of node.destroyed) {
cb();
}
}
__internal__destroyed.length = 0;
// Step 4: calling all mounted lifecycle hooks
let current;
let mountedFibers = this.mounted;
while ((current = mountedFibers.pop())) {
if (current.appliedToDom) {
for (let cb of current.node.mounted) {
cb();
}
}
}
// Step 5: calling all patched hooks
let patchedFibers = this.patched;
while ((current = patchedFibers.pop())) {
if (current.appliedToDom) {
for (let cb of current.node.patched) {
cb();
}
}
}
// unregistering the fiber
node.fiber = null;
} }
} }
type Position = "first-child" | "last-child"; export let __internal__destroyed: ComponentNode[] = [];
export interface MountOptions {
position?: Position;
}
export class MountFiber extends RootFiber { export class MountFiber extends RootFiber {
target: HTMLElement; target: HTMLElement;
position: Position;
constructor(node: ComponentNode, target: HTMLElement, options: MountOptions = {}) { constructor(node: ComponentNode, target: HTMLElement) {
super(node, null); super(node);
this.target = target; this.target = target;
this.position = options.position || "last-child";
} }
complete() { complete() {
let current: Fiber | undefined = this; const node = this.node;
try { node.bdom = this.bdom;
const node = this.node; mount(node.bdom!, this.target);
if (node.bdom) { node.status = STATUS.MOUNTED;
// this is a complicated situation: if we mount a fiber with an existing this.appliedToDom = true;
// bdom, this means that this same fiber was already completed, mounted, let current;
// but a crash occurred in some mounted hook. Then, it was handled and let mountedFibers = this.mounted;
// the new rendering is being applied. while ((current = mountedFibers.pop())) {
node.updateDom(); if (current.appliedToDom) {
} else { for (let cb of current.node.mounted) {
node.bdom = this.bdom; cb();
if (this.position === "last-child" || this.target.childNodes.length === 0) {
mount(node.bdom!, this.target);
} else {
const firstChild = this.target.childNodes[0];
mount(node.bdom!, this.target, firstChild);
} }
} }
// unregistering the fiber before mounted since it can do another render
// and that the current rendering is obviously completed
node.fiber = null;
node.status = STATUS.MOUNTED;
this.appliedToDom = true;
let mountedFibers = this.mounted;
while ((current = mountedFibers.pop())) {
if (current.appliedToDom) {
for (let cb of current.node.mounted) {
cb();
}
}
}
} catch (e) {
handleError({ fiber: current as Fiber, error: e });
} }
node.fiber = null;
} }
} }
+9 -35
View File
@@ -1,36 +1,10 @@
import { filterOutModifiersFromData } from "../blockdom/config"; export function mainEventHandler(data: any, ev: Event) {
if (typeof data === "function") {
export const mainEventHandler = (data: any, ev: Event, currentTarget?: EventTarget | null) => { data(ev);
const { data: _data, modifiers } = filterOutModifiersFromData(data); } else {
data = _data; const ctx = data[0];
let stopped = false; const method = data[1];
if (modifiers.length) { const args = data[2] || [];
let selfMode = false; ctx.__owl__.component[method](...args, ev);
const isSelf = ev.target === currentTarget;
for (const mod of modifiers) {
switch (mod) {
case "self":
selfMode = true;
if (isSelf) {
continue;
} else {
return stopped;
}
case "prevent":
if ((selfMode && isSelf) || !selfMode) ev.preventDefault();
continue;
case "stop":
if ((selfMode && isSelf) || !selfMode) ev.stopPropagation();
stopped = true;
continue;
}
}
} }
// If handler is empty, the array slot 0 will also be empty, and data will not have the property 0 }
// We check this rather than data[0] being truthy (or typeof function) so that it crashes
// as expected when there is a handler expression that evaluates to a falsy value
if (Object.hasOwnProperty.call(data, 0)) {
data[0].call(data[1] ? data[1].__owl__.component : null, ev);
}
return stopped;
};
-133
View File
@@ -1,133 +0,0 @@
import { Component } from "./component";
/**
* Apply default props (only top level).
*
* Note that this method does modify in place the props
*/
export function applyDefaultProps(props: { [key: string]: any }, ComponentClass: typeof Component) {
const defaultProps = (ComponentClass as any).defaultProps;
if (defaultProps) {
for (let propName in defaultProps) {
if (props![propName] === undefined) {
props![propName] = defaultProps[propName];
}
}
}
}
//------------------------------------------------------------------------------
// Prop validation helper
//------------------------------------------------------------------------------
function getPropDescription(staticProps: any) {
if (staticProps instanceof Array) {
return Object.fromEntries(
staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true]))
);
}
return staticProps || { "*": true };
}
/**
* Validate the component props (or next props) against the (static) props
* description. This is potentially an expensive operation: it may needs to
* visit recursively the props and all the children to check if they are valid.
* This is why it is only done in 'dev' mode.
*/
export const validateProps = function (name: string | typeof Component, props: any, parent?: any) {
const ComponentClass = (
typeof name !== "string" ? name : parent.constructor.components[name]
) as typeof Component;
applyDefaultProps(props, ComponentClass);
let propsDef = getPropDescription(ComponentClass.props);
const allowAdditionalProps = "*" in propsDef;
for (let propName in propsDef) {
if (propName === "*") {
continue;
}
if (props[propName] === undefined) {
if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
} else {
continue;
}
}
let isValid;
try {
isValid = isValidProp(props[propName], propsDef[propName]);
} catch (e) {
(e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${
(e as Error).message
})`;
throw e;
}
if (!isValid) {
throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
}
}
if (!allowAdditionalProps) {
for (let propName in props) {
if (!(propName in propsDef)) {
throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
}
}
}
};
/**
* Check if an invidual prop value matches its (static) prop definition
*/
function isValidProp(prop: any, propDef: any): boolean {
if (propDef === true) {
return true;
}
if (typeof propDef === "function") {
// Check if a value is constructed by some Constructor. Note that there is a
// slight abuse of language: we want to consider primitive values as well.
//
// So, even though 1 is not an instance of Number, we want to consider that
// it is valid.
if (typeof prop === "object") {
return prop instanceof propDef;
}
return typeof prop === propDef.name.toLowerCase();
} else if (propDef instanceof Array) {
// If this code is executed, this means that we want to check if a prop
// matches at least one of its descriptor.
let result = false;
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
result = result || isValidProp(prop, propDef[i]);
}
return result;
}
// propsDef is an object
if (propDef.optional && prop === undefined) {
return true;
}
let result = propDef.type ? isValidProp(prop, propDef.type) : true;
if (propDef.validate) {
result = result && propDef.validate(prop);
}
if (propDef.type === Array && propDef.element) {
for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element);
}
}
if (propDef.type === Object && propDef.shape) {
const shape = propDef.shape;
for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]);
}
if (result) {
for (let propName in prop) {
if (!(propName in shape)) {
throw new Error(`unknown prop '${propName}'`);
}
}
}
}
return result;
}
+10 -5
View File
@@ -1,4 +1,3 @@
import { fibersInError } from "./error_handling";
import { Fiber, RootFiber } from "./fibers"; import { Fiber, RootFiber } from "./fibers";
import { STATUS } from "./status"; import { STATUS } from "./status";
@@ -38,23 +37,29 @@ export class Scheduler {
flush() { flush() {
this.tasks.forEach((fiber) => { this.tasks.forEach((fiber) => {
if (fiber.root !== fiber) { if (fiber.root !== fiber) {
// this is wrong! should be something like
// if (this.tasks.has(fiber.root)) {
// // parent rendering has completed
// fiber.resolve();
// this.tasks.delete(fiber);
// }
this.tasks.delete(fiber); this.tasks.delete(fiber);
return; return;
} }
const hasError = fibersInError.has(fiber); if (fiber.error) {
if (hasError && fiber.counter !== 0) {
this.tasks.delete(fiber); this.tasks.delete(fiber);
fiber.reject(fiber.error);
return; return;
} }
if (fiber.node.status === STATUS.DESTROYED) { if (fiber.node.status === STATUS.DESTROYED) {
this.tasks.delete(fiber); this.tasks.delete(fiber);
return; return;
} }
if (fiber.counter === 0) { if (fiber.counter === 0) {
if (!hasError) { if (!fiber.error) {
fiber.complete(); fiber.complete();
} }
fiber.resolve();
this.tasks.delete(fiber); this.tasks.delete(fiber);
} }
}); });
+5
View File
@@ -0,0 +1,5 @@
export class EventBus extends EventTarget {
trigger(name: string, payload?: any) {
this.dispatchEvent(new CustomEvent(name, { detail: payload }));
}
}
-119
View File
@@ -1,119 +0,0 @@
import type { Env } from "./app/app";
import { getCurrent } from "./component/component_node";
import { onMounted, onPatched, onWillUnmount } from "./component/lifecycle_hooks";
// -----------------------------------------------------------------------------
// useRef
// -----------------------------------------------------------------------------
/**
* The purpose of this hook is to allow components to get a reference to a sub
* html node or component.
*/
export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el: T | null } {
const node = getCurrent()!;
const refs = node.refs;
return {
get el(): T | null {
return refs[name] || null;
},
};
}
// -----------------------------------------------------------------------------
// useEnv and useSubEnv
// -----------------------------------------------------------------------------
/**
* This hook is useful as a building block for some customized hooks, that may
* need a reference to the env of the component calling them.
*/
export function useEnv<E extends Env>(): E {
return getCurrent()!.component.env as any;
}
/**
* This hook is a simple way to let components use a sub environment. Note that
* like for all hooks, it is important that this is only called in the
* constructor method.
*/
export function useSubEnv(envExtension: Env) {
const node = getCurrent()!;
const env = Object.create(node.childEnv);
const descrs = Object.getOwnPropertyDescriptors(envExtension);
node.childEnv = Object.freeze(Object.defineProperties(env, descrs));
}
// -----------------------------------------------------------------------------
// useEffect
// -----------------------------------------------------------------------------
const NO_OP = () => {};
/**
* @param {...any} dependencies the dependencies computed by computeDependencies
* @returns {void|(()=>void)} a cleanup function that reverses the side
* effects of the effect callback.
*/
type Effect = (...dependencies: any[]) => void | (() => void);
/**
* This hook will run a callback when a component is mounted and patched, and
* will run a cleanup function before patching and before unmounting the
* the component.
*
* @param {Effect} effect the effect to run on component mount and/or patch
* @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute
* dependencies that will decide if the effect needs to be cleaned up and
* run again. If the dependencies did not change, the effect will not run
* again. The default value returns an array containing only NaN because
* NaN !== NaN, which will cause the effect to rerun on every patch.
*/
export function useEffect(effect: Effect, computeDependencies: () => any[] = () => [NaN]) {
let cleanup: () => void;
let dependencies: any[];
onMounted(() => {
dependencies = computeDependencies();
cleanup = effect(...dependencies) || NO_OP;
});
onPatched(() => {
const newDeps = computeDependencies();
const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
if (shouldReapply) {
dependencies = newDeps;
cleanup();
cleanup = effect(...dependencies) || NO_OP;
}
});
onWillUnmount(() => cleanup());
}
// -----------------------------------------------------------------------------
// 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 | typeof window,
eventName: string,
handler: EventListener,
eventParams?: AddEventListenerOptions
) {
const node = getCurrent()!;
const boundHandler = handler.bind(node.component);
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
}
+31 -15
View File
@@ -9,7 +9,6 @@ import {
remove, remove,
text, text,
toggler, toggler,
comment,
} from "./blockdom"; } from "./blockdom";
import { mainEventHandler } from "./component/handler"; import { mainEventHandler } from "./component/handler";
@@ -29,18 +28,37 @@ export const blockDom = {
toggler, toggler,
createBlock, createBlock,
html, html,
comment,
}; };
export { App, mount } from "./app/app"; // import { makeBlockClass } from "./_old_bdom/element";
export { Component } from "./component/component"; import { App } from "./app";
export { useComponent } from "./component/component_node"; import { Component } from "./component/component";
import { getCurrent } from "./component/component_node";
// import { getCurrent } from "./b_node";
export { App, Component };
export async function mount<T extends typeof Component>(
C: T,
target: HTMLElement
): Promise<InstanceType<T>> {
const app = new App(C);
return app.mount(target);
}
export function useComponent(): Component {
const current = getCurrent();
return current!.component;
}
export { status } from "./component/status"; export { status } from "./component/status";
export { Memo } from "./memo"; export { Portal } from "./misc/portal";
export { xml } from "./app/template_set"; export { Memo } from "./misc/memo";
export { useState, reactive } from "./reactivity"; export { xml } from "./tags";
export { useEffect, useEnv, useExternalListener, useRef, useSubEnv } from "./hooks"; export { useState } from "./reactivity";
export { EventBus, whenReady, loadFile, markup } from "./utils"; export { useRef } from "./refs";
export { EventBus } from "./event_bus";
export { export {
onWillStart, onWillStart,
onMounted, onMounted,
@@ -48,10 +66,8 @@ export {
onWillUpdateProps, onWillUpdateProps,
onWillPatch, onWillPatch,
onPatched, onPatched,
onWillRender, onRender,
onRendered, onDestroyed,
onWillDestroy, } from "./lifecycle_hooks";
onError,
} from "./component/lifecycle_hooks";
export const __info__ = {}; export const __info__ = {};
@@ -1,5 +1,4 @@
import { getCurrent } from "./component_node"; import { getCurrent } from "./component/component_node";
import { nodeErrorHandlers } from "./error_handling";
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// hooks // hooks
@@ -22,7 +21,7 @@ export function onMounted(fn: () => void | any) {
export function onWillPatch(fn: () => Promise<void> | any | void) { export function onWillPatch(fn: () => Promise<void> | any | void) {
const node = getCurrent()!; const node = getCurrent()!;
node.willPatch.unshift(fn); node.willPatch.push(fn);
} }
export function onPatched(fn: () => void | any) { export function onPatched(fn: () => void | any) {
@@ -32,15 +31,15 @@ export function onPatched(fn: () => void | any) {
export function onWillUnmount(fn: () => Promise<void> | void | any) { export function onWillUnmount(fn: () => Promise<void> | void | any) {
const node = getCurrent()!; const node = getCurrent()!;
node.willUnmount.unshift(fn); node.willUnmount.push(fn);
} }
export function onWillDestroy(fn: () => Promise<void> | void | any) { export function onDestroyed(fn: () => Promise<void> | void | any) {
const node = getCurrent()!; const node = getCurrent()!;
node.willDestroy.push(fn); node.destroyed.push(fn);
} }
export function onWillRender(fn: () => void | any) { export function onRender(fn: () => void | any) {
const node = getCurrent()!; const node = getCurrent()!;
const renderFn = node.renderFn; const renderFn = node.renderFn;
node.renderFn = () => { node.renderFn = () => {
@@ -48,24 +47,3 @@ export function onWillRender(fn: () => void | any) {
return renderFn(); return renderFn();
}; };
} }
export function onRendered(fn: () => void | any) {
const node = getCurrent()!;
const renderFn = node.renderFn;
node.renderFn = () => {
const result = renderFn();
fn();
return result;
};
}
type OnErrorCallback = (error: any) => void | any;
export function onError(callback: OnErrorCallback) {
const node = getCurrent()!;
let handlers = nodeErrorHandlers.get(node);
if (!handlers) {
handlers = [];
nodeErrorHandlers.set(node, handlers);
}
handlers.push(callback);
}
+5 -5
View File
@@ -1,7 +1,7 @@
import { Component } from "./component/component"; import { Component } from "../component/component";
import type { ComponentNode } from "./component/component_node"; import type { ComponentNode } from "../component/component_node";
import { xml } from "./app/template_set"; import { xml } from "../tags";
import { Fiber } from "./component/fibers"; import { Fiber } from "../component/fibers";
export class Memo extends Component { export class Memo extends Component {
static template = xml`<t t-slot="default"/>`; static template = xml`<t t-slot="default"/>`;
@@ -39,7 +39,7 @@ export class Memo extends Component {
*/ */
function shallowEqual(p1: any, p2: any): boolean { function shallowEqual(p1: any, p2: any): boolean {
for (let k in p1) { for (let k in p1) {
if (k !== "slots" && p1[k] !== p2[k]) { if (p1[k] !== p2[k]) {
return false; return false;
} }
} }
+19 -5
View File
@@ -1,21 +1,22 @@
import { BDom, text, VNode } from "./blockdom"; import type { ComponentNode } from "../component/component_node";
import { Component } from "../component/component";
import { xml } from "../tags";
import { BDom, text, VNode } from "../blockdom";
const VText: any = text("").constructor; const VText: any = text("").constructor;
export class VPortal extends VText implements Partial<VNode<VPortal>> { class VPortal extends VText implements Partial<VNode<VPortal>> {
// selector: string; // selector: string;
realBDom: BDom | null; realBDom: BDom | null;
target: HTMLElement | null = null; target: HTMLElement | null = null;
constructor(selector: string, realBDom: BDom, ownerComponent: ComponentNode) { constructor(selector: string, realBDom: BDom) {
super(""); super("");
this.ownerComponent = ownerComponent;
this.selector = selector; this.selector = selector;
this.realBDom = realBDom; this.realBDom = realBDom;
} }
mount(parent: HTMLElement, anchor: ChildNode) { mount(parent: HTMLElement, anchor: ChildNode) {
super.mount(parent, anchor); super.mount(parent, anchor);
this.ownerComponent.willDestroy.push(() => this.cleanup());
this.target = document.querySelector(this.selector) as any; this.target = document.querySelector(this.selector) as any;
if (!this.target) { if (!this.target) {
let el: any = this.el; let el: any = this.el;
@@ -49,3 +50,16 @@ export class VPortal extends VText implements Partial<VNode<VPortal>> {
} }
} }
} }
export class Portal extends Component {
static template = xml`<t t-slot="default"/>`;
constructor(props: any, env: any, node: ComponentNode) {
super(props, env, node);
node._render = function (fiber: any) {
const bdom = new VPortal(props.target, this.renderFn());
fiber.bdom = bdom;
fiber.root.counter--;
};
}
}
File diff suppressed because it is too large Load Diff
@@ -70,7 +70,6 @@ interface Token {
size?: number; size?: number;
varName?: string; varName?: string;
replace?: Function; replace?: Function;
isLocal?: boolean;
} }
const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(null), { const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(null), {
@@ -326,13 +325,6 @@ export function compileExprToArray(expr: string): Token[] {
} }
i++; i++;
} }
// Mark all variables that have been used locally.
// This assumes the expression has only one scope (incorrect but "good enough for now")
for (const token of tokens) {
if (token.type === "SYMBOL" && localVars.has(token.value)) {
token.isLocal = true;
}
}
return tokens; return tokens;
} }
+172 -371
View File
@@ -11,7 +11,7 @@ export const enum ASTType {
TIf, TIf,
TSet, TSet,
TCall, TCall,
TOut, TRaw,
TForEach, TForEach,
TKey, TKey,
TComponent, TComponent,
@@ -19,8 +19,6 @@ export const enum ASTType {
TLog, TLog,
TSlot, TSlot,
TCallBlock, TCallBlock,
TTranslation,
TPortal,
} }
export interface ASTText { export interface ASTText {
@@ -36,27 +34,15 @@ export interface ASTComment {
export interface ASTDomNode { export interface ASTDomNode {
type: ASTType.DomNode; type: ASTType.DomNode;
tag: string; tag: string;
dynamicTag: string | null;
attrs: { [key: string]: string }; attrs: { [key: string]: string };
content: AST[]; content: AST[];
ref: string | null; ref: string | null;
on: { [key: string]: string }; on: { [key: string]: string };
model: {
baseExpr: string;
expr: string;
targetAttr: string;
specialInitTargetAttr: string | null;
eventType: "change" | "click" | "input";
shouldTrim: boolean;
shouldNumberize: boolean;
} | null;
ns: string | null;
} }
export interface ASTMulti { export interface ASTMulti {
type: ASTType.Multi; type: ASTType.Multi;
content: AST[]; content: AST[];
deepRemove: boolean;
} }
export interface ASTTEsc { export interface ASTTEsc {
@@ -65,8 +51,8 @@ export interface ASTTEsc {
defaultValue: string; defaultValue: string;
} }
export interface ASTTOut { export interface ASTTRaw {
type: ASTType.TOut; type: ASTType.TRaw;
expr: string; expr: string;
body: AST[] | null; body: AST[] | null;
} }
@@ -77,7 +63,6 @@ export interface ASTTif {
content: AST; content: AST;
tElif: { condition: string; content: AST }[] | null; tElif: { condition: string; content: AST }[] | null;
tElse: AST | null; tElse: AST | null;
deepRemove: boolean;
} }
export interface ASTTSet { export interface ASTTSet {
@@ -95,7 +80,8 @@ export interface ASTTForEach {
key: string | null; key: string | null;
body: AST; body: AST;
memo: string; memo: string;
deepRemove: boolean; isOnlyChild: boolean;
hasNoComponent: boolean;
hasNoFirst: boolean; hasNoFirst: boolean;
hasNoLast: boolean; hasNoLast: boolean;
hasNoIndex: boolean; hasNoIndex: boolean;
@@ -118,15 +104,14 @@ export interface ASTComponent {
type: ASTType.TComponent; type: ASTType.TComponent;
name: string; name: string;
isDynamic: boolean; isDynamic: boolean;
dynamicProps: string | null;
props: { [name: string]: string }; props: { [name: string]: string };
slots: { [name: string]: { content: AST; attrs?: { [key: string]: string }; scope?: string } }; handlers: { [event: string]: string };
slots: { [name: string]: AST };
} }
export interface ASTSlot { export interface ASTSlot {
type: ASTType.TSlot; type: ASTType.TSlot;
name: string; name: string;
attrs: { [key: string]: string };
defaultContent: AST | null; defaultContent: AST | null;
} }
@@ -146,17 +131,6 @@ export interface ASTLog {
content: AST | null; content: AST | null;
} }
export interface ASTTranslation {
type: ASTType.TTranslation;
content: AST | null;
}
export interface ASTTPortal {
type: ASTType.TPortal;
target: string;
content: AST;
}
export type AST = export type AST =
| ASTText | ASTText
| ASTComment | ASTComment
@@ -166,30 +140,27 @@ export type AST =
| ASTTif | ASTTif
| ASTTSet | ASTTSet
| ASTTCall | ASTTCall
| ASTTOut | ASTTRaw
| ASTTForEach | ASTTForEach
| ASTTKey | ASTTKey
| ASTComponent | ASTComponent
| ASTSlot | ASTSlot
| ASTTCallBlock | ASTTCallBlock
| ASTLog | ASTLog
| ASTDebug | ASTDebug;
| ASTTranslation
| ASTTPortal;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Parser // Parser
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
interface ParsingContext { interface ParsingContext {
inPreTag: boolean; inPreTag: boolean;
inSVG: boolean;
} }
export function parse(xml: string | Node): AST { export function parse(xml: string): AST {
const node = xml instanceof Element ? xml : (parseXML(`<t>${xml}</t>`).firstChild! as Element); const template = `<t>${xml}</t>`;
normalizeXML(node); const doc = parseXML(template);
const ctx = { inPreTag: false, inSVG: false }; const ctx = { inPreTag: false };
const ast = parseNode(node, ctx); const ast = parseNode(doc.firstChild!, ctx);
if (!ast) { if (!ast) {
return { type: ASTType.Text, value: "" }; return { type: ASTType.Text, value: "" };
} }
@@ -204,14 +175,12 @@ function parseNode(node: ChildNode, ctx: ParsingContext): AST | null {
parseTDebugLog(node, ctx) || parseTDebugLog(node, ctx) ||
parseTForEach(node, ctx) || parseTForEach(node, ctx) ||
parseTIf(node, ctx) || parseTIf(node, ctx) ||
parseTPortal(node, ctx) ||
parseTCall(node, ctx) || parseTCall(node, ctx) ||
parseTCallBlock(node, ctx) || parseTCallBlock(node, ctx) ||
parseTEscNode(node, ctx) || parseTEscNode(node, ctx) ||
parseTKey(node, ctx) || parseTKey(node, ctx) ||
parseTTranslation(node, ctx) ||
parseTSlot(node, ctx) || parseTSlot(node, ctx) ||
parseTOutNode(node, ctx) || parseTRawNode(node, ctx) ||
parseComponent(node, ctx) || parseComponent(node, ctx) ||
parseDOMNode(node, ctx) || parseDOMNode(node, ctx) ||
parseTSetNode(node, ctx) || parseTSetNode(node, ctx) ||
@@ -227,7 +196,24 @@ function parseTNode(node: Element, ctx: ParsingContext): AST | null {
if (node.tagName !== "t") { if (node.tagName !== "t") {
return null; return null;
} }
return parseChildNodes(node, ctx); const children: AST[] = [];
for (let child of node.childNodes) {
const ast = parseNode(child, ctx);
if (ast) {
children.push(ast);
}
}
switch (children.length) {
case 0:
return null;
case 1:
return children[0];
default:
return {
type: ASTType.Multi,
content: children,
};
}
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -237,7 +223,7 @@ const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g; const whitespaceRE = /\s+/g;
function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null { function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null {
if (node.nodeType === Node.TEXT_NODE) { if (node.nodeType === 3) {
let value = node.textContent || ""; let value = node.textContent || "";
if (!ctx.inPreTag) { if (!ctx.inPreTag) {
if (lineBreakRE.test(value) && !value.trim()) { if (lineBreakRE.test(value) && !value.trim()) {
@@ -247,7 +233,7 @@ function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null
} }
return { type: ASTType.Text, value }; return { type: ASTType.Text, value };
} else if (node.nodeType === Node.COMMENT_NODE) { } else if (node.nodeType === 8) {
return { type: ASTType.Comment, value: node.textContent || "" }; return { type: ASTType.Comment, value: node.textContent || "" };
} }
return null; return null;
@@ -281,79 +267,38 @@ function parseTDebugLog(node: Element, ctx: ParsingContext): AST | null {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Regular dom node // Regular dom node
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
const hasDotAtTheEnd = /\.[\w_]+\s*$/;
const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
function parseDOMNode(node: Element, ctx: ParsingContext): AST | null { function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
const { tagName } = node; if (node.tagName === "t") {
const dynamicTag = node.getAttribute("t-tag");
node.removeAttribute("t-tag");
if (tagName === "t" && !dynamicTag) {
return null; return null;
} }
ctx = Object.assign({}, ctx); const children: AST[] = [];
if (tagName === "pre") { if (node.tagName === "pre") {
ctx.inPreTag = true; ctx = { inPreTag: true };
}
let ref = null;
if (node.hasAttribute("t-ref")) {
ref = node.getAttribute("t-ref");
node.removeAttribute("t-ref");
} }
const shouldAddSVGNS = tagName === "svg" || (tagName === "g" && !ctx.inSVG);
ctx.inSVG = ctx.inSVG || shouldAddSVGNS;
const ns = shouldAddSVGNS ? "http://www.w3.org/2000/svg" : null;
const ref = node.getAttribute("t-ref");
node.removeAttribute("t-ref");
const children = parseChildren(node, ctx); for (let child of node.childNodes) {
const ast = parseNode(child, ctx);
if (ast) {
children.push(ast);
}
}
const nodeAttrsNames = node.getAttributeNames();
const attrs: ASTDomNode["attrs"] = {}; const attrs: ASTDomNode["attrs"] = {};
const on: ASTDomNode["on"] = {}; const on: ASTDomNode["on"] = {};
let model: ASTDomNode["model"] = null;
for (let attr of nodeAttrsNames) { for (let attr of node.getAttributeNames()) {
const value = node.getAttribute(attr)!; const value = node.getAttribute(attr)!;
if (attr.startsWith("t-on")) { if (attr.startsWith("t-on")) {
if (attr === "t-on") { if (attr === "t-on") {
throw new Error("Missing event name with t-on directive"); throw new Error("Missing event name with t-on directive");
} }
on[attr.slice(5)] = value; on[attr.slice(5)] = value;
} else if (attr.startsWith("t-model")) {
if (!["input", "select", "textarea"].includes(tagName)) {
throw new Error("The t-model directive only works with <input>, <textarea> and <select>");
}
let baseExpr, expr;
if (hasDotAtTheEnd.test(value)) {
const index = value.lastIndexOf(".");
baseExpr = value.slice(0, index);
expr = `'${value.slice(index + 1)}'`;
} else if (hasBracketsAtTheEnd.test(value)) {
const index = value.lastIndexOf("[");
baseExpr = value.slice(0, index);
expr = value.slice(index + 1, -1);
} else {
throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
}
const typeAttr = node.getAttribute("type");
const isInput = tagName === "input";
const isSelect = tagName === "select";
const isTextarea = tagName === "textarea";
const isCheckboxInput = isInput && typeAttr === "checkbox";
const isRadioInput = isInput && typeAttr === "radio";
const isOtherInput = isInput && !isCheckboxInput && !isRadioInput;
const hasLazyMod = attr.includes(".lazy");
const hasNumberMod = attr.includes(".number");
const hasTrimMod = attr.includes(".trim");
const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
model = {
baseExpr,
expr,
targetAttr: isCheckboxInput ? "checked" : "value",
specialInitTargetAttr: isRadioInput ? "checked" : null,
eventType,
shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
};
} else { } else {
if (attr.startsWith("t-") && !attr.startsWith("t-att")) { if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
throw new Error(`Unknown QWeb directive: '${attr}'`); throw new Error(`Unknown QWeb directive: '${attr}'`);
@@ -361,16 +306,16 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
attrs[attr] = value; attrs[attr] = value;
} }
} }
if (children.length === 1 && children[0].type === ASTType.TForEach) {
children[0].isOnlyChild = true;
}
return { return {
type: ASTType.DomNode, type: ASTType.DomNode,
tag: tagName, tag: node.tagName,
dynamicTag,
attrs, attrs,
on, on,
ref, ref,
content: children, content: children,
model,
ns,
}; };
} }
@@ -395,53 +340,56 @@ function parseTEscNode(node: Element, ctx: ParsingContext): AST | null {
if (!ast) { if (!ast) {
return tesc; return tesc;
} }
if (ast.type === ASTType.DomNode) { if (ast && ast.type === ASTType.DomNode) {
return { return {
...ast, type: ASTType.DomNode,
tag: ast.tag,
attrs: ast.attrs,
on: ast.on,
ref, ref,
content: [tesc], content: [tesc],
}; };
} }
if (ast.type === ASTType.TComponent) { if (ast && ast.type === ASTType.TComponent) {
throw new Error("t-esc is not supported on Component nodes"); return {
...ast,
slots: { default: tesc },
};
} }
return tesc; return tesc;
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// t-out // t-raw
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
function parseTOutNode(node: Element, ctx: ParsingContext): AST | null { function parseTRawNode(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-out") && !node.hasAttribute("t-raw")) { if (!node.hasAttribute("t-raw")) {
return null; return null;
} }
if (node.hasAttribute("t-raw")) { const expr = node.getAttribute("t-raw")!;
console.warn(
`t-raw has been deprecated in favor of t-out. If the value to render is not wrapped by the "markup" function, it will be escaped`
);
}
const expr = (node.getAttribute("t-out") || node.getAttribute("t-raw"))!;
node.removeAttribute("t-out");
node.removeAttribute("t-raw"); node.removeAttribute("t-raw");
const tOut: AST = { type: ASTType.TOut, expr, body: null }; const tRaw: AST = { type: ASTType.TRaw, expr, body: null };
const ref = node.getAttribute("t-ref"); const ref = node.getAttribute("t-ref");
node.removeAttribute("t-ref"); node.removeAttribute("t-ref");
const ast = parseNode(node, ctx); const ast = parseNode(node, ctx);
if (!ast) { if (!ast) {
return tOut; return tRaw;
} }
if (ast.type === ASTType.DomNode) { if (ast && ast.type === ASTType.DomNode) {
tOut.body = ast.content.length ? ast.content : null; tRaw.body = ast.content.length ? ast.content : null;
return { return {
...ast, type: ASTType.DomNode,
tag: ast.tag,
attrs: ast.attrs,
on: ast.on,
ref, ref,
content: [tOut], content: [tRaw],
}; };
} }
return tOut; return tRaw;
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -458,11 +406,6 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
const elem = node.getAttribute("t-as") || ""; const elem = node.getAttribute("t-as") || "";
node.removeAttribute("t-as"); node.removeAttribute("t-as");
const key = node.getAttribute("t-key"); const key = node.getAttribute("t-key");
if (!key) {
throw new Error(
`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`
);
}
node.removeAttribute("t-key"); node.removeAttribute("t-key");
const memo = node.getAttribute("t-memo") || ""; const memo = node.getAttribute("t-memo") || "";
node.removeAttribute("t-memo"); node.removeAttribute("t-memo");
@@ -484,8 +427,9 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
elem, elem,
body, body,
memo, memo,
deepRemove: needDeepRemove(body),
key, key,
isOnlyChild: false,
hasNoComponent: hasNoComponent(body),
hasNoFirst, hasNoFirst,
hasNoLast, hasNoLast,
hasNoIndex, hasNoIndex,
@@ -494,40 +438,53 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
} }
/** /**
* @returns true if we are sure that a deep remove (without optimisation) is needed, for exemple * @returns true if we are sure the ast does not contain any component
* if there is a portal.
*/ */
function needDeepRemove(ast: AST): boolean { function hasNoComponent(ast: AST): boolean {
switch (ast.type) { switch (ast.type) {
case ASTType.Multi:
case ASTType.TForEach:
case ASTType.TIf:
return ast.deepRemove;
case ASTType.TPortal:
return true;
case ASTType.TComponent: case ASTType.TComponent:
case ASTType.TOut: case ASTType.TRaw:
case ASTType.TCall: case ASTType.TCall:
case ASTType.TCallBlock: case ASTType.TCallBlock:
case ASTType.TSlot: case ASTType.TSlot:
return false;
case ASTType.TSet:
case ASTType.Text: case ASTType.Text:
case ASTType.Comment: case ASTType.Comment:
case ASTType.TEsc: case ASTType.TEsc:
return false; return true;
case ASTType.TKey: case ASTType.TKey:
return needDeepRemove(ast.content); return hasNoComponent(ast.content);
case ASTType.TDebug: case ASTType.TDebug:
case ASTType.TLog: case ASTType.TLog:
case ASTType.TTranslation: return ast.content ? hasNoComponent(ast.content) : true;
return ast.content ? needDeepRemove(ast.content) : false; case ASTType.TForEach:
case ASTType.TSet: return ast.hasNoComponent;
return ast.body ? ast.body.some((ast) => needDeepRemove(ast)) : false; case ASTType.Multi:
case ASTType.DomNode: {
case ASTType.DomNode: for (let elem of ast.content) {
return ast.content.some((ast) => needDeepRemove(ast)); if (!hasNoComponent(elem)) {
return false;
}
}
return true;
}
case ASTType.TIf: {
if (!hasNoComponent(ast.content)) {
return false;
}
if (ast.tElif) {
for (let elem of ast.tElif) {
if (!hasNoComponent(elem.content)) {
return false;
}
}
}
if (ast.tElse && !hasNoComponent(ast.tElse)) {
return false;
}
return true;
}
} }
} }
@@ -565,11 +522,17 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
if (ast && ast.type === ASTType.TComponent) { if (ast && ast.type === ASTType.TComponent) {
return { return {
...ast, ...ast,
slots: { default: { content: tcall } }, slots: { default: tcall },
}; };
} }
} }
const body = parseChildren(node, ctx); const body: AST[] = [];
for (let child of node.childNodes) {
const ast = parseNode(child, ctx);
if (ast) {
body.push(ast);
}
}
return { return {
type: ASTType.TCall, type: ASTType.TCall,
@@ -603,7 +566,10 @@ function parseTIf(node: Element, ctx: ParsingContext): AST | null {
} }
const condition = node.getAttribute("t-if")!; const condition = node.getAttribute("t-if")!;
node.removeAttribute("t-if"); node.removeAttribute("t-if");
const content = parseNode(node, ctx) || { type: ASTType.Text, value: "" }; const content = parseNode(node, ctx);
if (!content) {
throw new Error("hmmm");
}
let nextElement = node.nextElementSibling; let nextElement = node.nextElementSibling;
// t-elifs // t-elifs
@@ -628,21 +594,12 @@ function parseTIf(node: Element, ctx: ParsingContext): AST | null {
nextElement.remove(); nextElement.remove();
} }
let deepRemove = needDeepRemove(content);
if (tElifs) {
deepRemove = deepRemove || tElifs.some((ast) => needDeepRemove(ast));
}
if (tElse) {
deepRemove = deepRemove || needDeepRemove(tElse);
}
return { return {
type: ASTType.TIf, type: ASTType.TIf,
condition, condition,
content, content,
tElif: tElifs.length ? tElifs : null, tElif: tElifs.length ? tElifs : null,
tElse, tElse,
deepRemove,
}; };
} }
@@ -659,7 +616,13 @@ function parseTSetNode(node: Element, ctx: ParsingContext): AST | null {
const defaultValue = node.innerHTML === node.textContent ? node.textContent || null : null; const defaultValue = node.innerHTML === node.textContent ? node.textContent || null : null;
let body: AST[] | null = null; let body: AST[] | null = null;
if (node.textContent !== node.innerHTML) { if (node.textContent !== node.innerHTML) {
body = parseChildren(node, ctx); body = [];
for (let child of node.childNodes) {
let childAst = parseNode(child, ctx);
if (childAst) {
body.push(childAst);
}
}
} }
return { type: ASTType.TSet, name, value, defaultValue, body }; return { type: ASTType.TSet, name, value, defaultValue, body };
} }
@@ -668,29 +631,11 @@ function parseTSetNode(node: Element, ctx: ParsingContext): AST | null {
// Components // Components
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Error messages when trying to use an unsupported directive on a component
const directiveErrorMap = new Map([
["t-on", "t-on is no longer supported on components. Consider passing a callback in props."],
[
"t-ref",
"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.",
],
["t-att", "t-att makes no sense on component: props are already treated as expressions"],
[
"t-attf",
"t-attf is not supported on components: use template strings for string interpolation in props",
],
]);
function parseComponent(node: Element, ctx: ParsingContext): AST | null { function parseComponent(node: Element, ctx: ParsingContext): AST | null {
let name = node.tagName; let name = node.tagName;
const firstLetter = name[0]; const firstLetter = name[0];
let isDynamic = node.hasAttribute("t-component"); let isDynamic = node.hasAttribute("t-component");
if (isDynamic && name !== "t") {
throw new Error(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);
}
if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) { if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
return null; return null;
} }
@@ -699,15 +644,12 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
node.removeAttribute("t-component"); node.removeAttribute("t-component");
} }
const dynamicProps = node.getAttribute("t-props");
node.removeAttribute("t-props");
const props: ASTComponent["props"] = {}; const props: ASTComponent["props"] = {};
const handlers: ASTComponent["handlers"] = {};
for (let name of node.getAttributeNames()) { for (let name of node.getAttributeNames()) {
const value = node.getAttribute(name)!; const value = node.getAttribute(name)!;
if (name.startsWith("t-")) { if (name.startsWith("t-on-")) {
const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-")); handlers[name.slice(5)] = value;
throw new Error(message || `unsupported directive on Component: ${name}`);
} else { } else {
props[name] = value; props[name] = value;
} }
@@ -720,11 +662,6 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
// named slots // named slots
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]")); const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
for (let slotNode of slotNodes) { for (let slotNode of slotNodes) {
if (slotNode.tagName !== "t") {
throw new Error(
`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`
);
}
const name = slotNode.getAttribute("t-set-slot")!; const name = slotNode.getAttribute("t-set-slot")!;
// check if this is defined in a sub component (in which case it should // check if this is defined in a sub component (in which case it should
@@ -746,30 +683,17 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
slotNode.remove(); slotNode.remove();
const slotAst = parseNode(slotNode, ctx); const slotAst = parseNode(slotNode, ctx);
if (slotAst) { if (slotAst) {
const slotInfo: any = { content: slotAst }; slots[name] = slotAst;
const attrs: { [key: string]: string } = {};
for (let attributeName of slotNode.getAttributeNames()) {
const value = slotNode.getAttribute(attributeName)!;
if (attributeName === "t-slot-scope") {
slotInfo.scope = value;
continue;
}
attrs[attributeName] = value;
}
if (Object.keys(attrs).length) {
slotInfo.attrs = attrs;
}
slots[name] = slotInfo;
} }
} }
// default slot // default slot
const defaultContent = parseChildNodes(clone, ctx); const defaultContent = parseChildNodes(clone, ctx);
if (defaultContent) { if (defaultContent) {
slots.default = { content: defaultContent }; slots.default = defaultContent;
} }
} }
return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots }; return { type: ASTType.TComponent, name, isDynamic, props, handlers, slots };
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -780,113 +704,62 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-slot")) { if (!node.hasAttribute("t-slot")) {
return null; return null;
} }
const name = node.getAttribute("t-slot")!;
node.removeAttribute("t-slot");
const attrs: { [key: string]: string } = {};
for (let attributeName of node.getAttributeNames()) {
const value = node.getAttribute(attributeName)!;
attrs[attributeName] = value;
}
return { return {
type: ASTType.TSlot, type: ASTType.TSlot,
name, name: node.getAttribute("t-slot")!,
attrs,
defaultContent: parseChildNodes(node, ctx), defaultContent: parseChildNodes(node, ctx),
}; };
} }
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
if (node.getAttribute("t-translation") !== "off") {
return null;
}
node.removeAttribute("t-translation");
return {
type: ASTType.TTranslation,
content: parseNode(node, ctx),
};
}
// -----------------------------------------------------------------------------
// Portal
// -----------------------------------------------------------------------------
function parseTPortal(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-portal")) {
return null;
}
if (node.tagName !== "t") {
throw new Error(
`Directive 't-portal' can only be used on <t> nodes (used on a <${node.tagName}>)`
);
}
const target = node.getAttribute("t-portal")!;
node.removeAttribute("t-portal");
const content = parseNode(node, ctx);
if (!content) {
return {
type: ASTType.Text,
value: "",
};
}
return {
type: ASTType.TPortal,
target,
content,
};
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// helpers // helpers
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
/** function parseChildNodes(node: Element, ctx: ParsingContext): AST | null {
* Parse all the child nodes of a given node and return a list of ast elements
*/
function parseChildren(node: Node, ctx: ParsingContext): AST[] {
const children: AST[] = []; const children: AST[] = [];
for (let child of node.childNodes) { for (let child of node.childNodes) {
const childAst = parseNode(child, ctx); const childAst = parseNode(child, ctx);
if (childAst) { if (childAst) {
if (childAst.type === ASTType.Multi) { children.push(childAst);
children.push(...childAst.content);
} else {
children.push(childAst);
}
} }
} }
return children;
}
/**
* Parse all the child nodes of a given node and return an ast if possible.
* In the case there are multiple children, they are wrapped in a astmulti.
*/
function parseChildNodes(node: Node, ctx: ParsingContext): AST | null {
const children = parseChildren(node, ctx);
switch (children.length) { switch (children.length) {
case 0: case 0:
return null; return null;
case 1: case 1:
return children[0]; return children[0];
default: default:
return { return { type: ASTType.Multi, content: children };
type: ASTType.Multi,
content: children,
deepRemove: children.some((ast) => needDeepRemove(ast)),
};
} }
} }
function parseXML(xml: string): Document {
const parser = new DOMParser();
/** const doc = parser.parseFromString(xml, "text/xml");
* Normalizes the content of an Element so that t-if/t-elif/t-else directives if (doc.getElementsByTagName("parsererror").length) {
* immediately follow one another (by removing empty text nodes or comments). let msg = "Invalid XML in template.";
* Throws an error when a conditional branching statement is malformed. This const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
* function modifies the Element in place. if (parsererrorText) {
* msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
* @param el the element containing the tree that should be normalized const re = /\d+/g;
*/ const firstMatch = re.exec(parsererrorText);
function normalizeTIf(el: Element) { if (firstMatch) {
let tbranch = el.querySelectorAll("[t-elif], [t-else]"); const lineNumber = Number(firstMatch[0]);
const line = xml.split("\n")[lineNumber - 1];
const secondMatch = re.exec(parsererrorText);
if (line && secondMatch) {
const columnIndex = Number(secondMatch[0]) - 1;
if (line[columnIndex]) {
msg +=
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
`${line}\n${"-".repeat(columnIndex - 1)}^`;
}
}
}
}
throw new Error(msg);
}
let tbranch = doc.querySelectorAll("[t-elif], [t-else]");
for (let i = 0, ilen = tbranch.length; i < ilen; i++) { for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
let node = tbranch[i]; let node = tbranch[i];
let prevElem = node.previousElementSibling!; let prevElem = node.previousElementSibling!;
@@ -920,78 +793,6 @@ function normalizeTIf(el: Element) {
); );
} }
} }
}
/**
* Normalizes the content of an Element so that t-esc directives on components
* are removed and instead places a <t t-esc=""> as the default slot of the
* component. Also throws if the component already has content. This function
* modifies the Element in place.
*
* @param el the element containing the tree that should be normalized
*/
function normalizeTEsc(el: Element) {
const elements = [...el.querySelectorAll("[t-esc]")].filter(
(el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component")
);
for (const el of elements) {
if (el.childNodes.length) {
throw new Error("Cannot have t-esc on a component that already has content");
}
const value = el.getAttribute("t-esc");
el.removeAttribute("t-esc");
const t = el.ownerDocument.createElement("t");
if (value != null) {
t.setAttribute("t-esc", value);
}
el.appendChild(t);
}
}
/**
* Normalizes the tree inside a given element and do some preliminary validation
* on it. This function modifies the Element in place.
*
* @param el the element containing the tree that should be normalized
*/
function normalizeXML(el: Element) {
normalizeTIf(el);
normalizeTEsc(el);
}
/**
* Parses an XML string into an XML document, throwing errors on parser errors
* instead of returning an XML document containing the parseerror.
*
* @param xml the string to parse
* @returns an XML document corresponding to the content of the string
*/
function parseXML(xml: string): XMLDocument {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) {
let msg = "Invalid XML in template.";
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
if (parsererrorText) {
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
const re = /\d+/g;
const firstMatch = re.exec(parsererrorText);
if (firstMatch) {
const lineNumber = Number(firstMatch[0]);
const line = xml.split("\n")[lineNumber - 1];
const secondMatch = re.exec(parsererrorText);
if (line && secondMatch) {
const columnIndex = Number(secondMatch[0]) - 1;
if (line[columnIndex]) {
msg +=
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
`${line}\n${"-".repeat(columnIndex - 1)}^`;
}
}
}
}
throw new Error(msg);
}
return doc; return doc;
} }
+129
View File
@@ -0,0 +1,129 @@
// import { compileTemplate, Template } from "./qweb/index";
import { BDom, createBlock, html, list, multi, text, toggler } from "../blockdom";
import { component } from "../component/component_node";
import { Template, compileTemplate } from "./compiler";
const bdom = { text, createBlock, list, multi, html, toggler, component };
export const globalTemplates: { [key: string]: string } = {};
function withDefault(value: any, defaultValue: any): any {
return value === undefined || value === null || value === false ? defaultValue : value;
}
function callSlot(
ctx: any,
parent: any,
key: string,
name: string,
defaultSlot?: (ctx: any, key: string) => BDom,
dynamic?: boolean
): BDom | null {
const slots = ctx.__owl__.slots;
const slotFn = slots[name];
const slotBDom = slotFn ? slotFn(parent, key) : null;
if (defaultSlot) {
let child1: BDom | undefined = undefined;
let child2: BDom | undefined = undefined;
// const result = new BMulti(2);
if (slotBDom) {
child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
} else {
child2 = defaultSlot(parent, key);
}
return multi([child1, child2]);
}
return slotBDom;
}
function capture(ctx: any): any {
const component = ctx.__owl__.component;
const result = Object.create(component);
for (let k in ctx) {
result[k] = ctx[k];
}
return result;
}
function withKey(elem: any, k: string) {
elem.key = k;
return elem;
}
function prepareList(collection: any): [any[], any[], number, any[]] {
let keys: any[];
let values: any[];
if (Array.isArray(collection)) {
keys = collection;
values = collection;
} else if (collection) {
values = Object.keys(collection);
keys = Object.values(collection);
} else {
throw new Error("Invalid loop expression");
}
const n = values.length;
return [keys, values, n, new Array(n)];
}
export const UTILS = {
// elem,
// setText,
withDefault,
zero: Symbol("zero"),
callSlot,
capture,
// toClassObj,
withKey,
prepareList,
shallowEqual,
};
export class TemplateSet {
rawTemplates: { [name: string]: string } = Object.create(globalTemplates);
templates: { [name: string]: Template } = {};
utils: typeof UTILS;
constructor() {
const call = (subTemplate: string, ctx: any, parent: any) => {
const template = this.getTemplate(subTemplate);
return toggler(subTemplate, template(ctx, parent));
};
const getTemplate = (name: string) => this.getTemplate(name);
this.utils = Object.assign({}, UTILS, { getTemplate, call });
}
addTemplate(name: string, template: string, options: { allowDuplicate?: boolean } = {}) {
if (name in this.rawTemplates && !options.allowDuplicate) {
throw new Error(`Template ${name} already defined`);
}
this.rawTemplates[name] = template;
}
getTemplate(name: string): Template {
if (!(name in this.templates)) {
const rawTemplate = this.rawTemplates[name];
if (rawTemplate === undefined) {
throw new Error(`Missing template: "${name}"`);
}
const templateFn = compileTemplate(rawTemplate, name);
// first add a function to lazily get the template, in case there is a
// recursive call to the template name
this.templates[name] = (context, parent) => this.templates[name](context, parent);
const template = templateFn(bdom, this.utils);
this.templates[name] = template;
}
return this.templates[name];
}
}
function shallowEqual(l1: any[], l2: any[]): boolean {
for (let i = 0, l = l1.length; i < l; i++) {
if (l1[i] !== l2[i]) {
return false;
}
}
return true;
}
+129 -226
View File
@@ -1,241 +1,144 @@
import { onWillUnmount } from "./component/lifecycle_hooks"; import { getCurrent } from "./component/component_node";
import { ComponentNode, getCurrent } from "./component/component_node";
// Allows to get the target of a Reactive (used for making a new Reactive from the underlying object) export function useState<T>(state: T): T {
const TARGET = Symbol("Target"); const node = getCurrent()!;
// Special key to subscribe to, to be notified of key creation/deletion return observe(state, () => node.render());
const KEYCHANGES = Symbol("Key changes"); }
type ObjectKey = string | number | symbol; type CB = () => void;
type Target = object; const observers: WeakMap<any, PSet<CB>> = new WeakMap();
type Callback = () => void;
type Reactive<T extends Target = Target> = T & {
[TARGET]: any;
};
/** /**
* Checks whether a given value can be made into a reactive object. * PSet (for Prototypal Set) are sets that can lookup in their "parent sets", if
* * any.
* @param value the value to check
* @returns whether the value can be made reactive
*/ */
function canBeMadeReactive(value: any): boolean {
class PSet<T> extends Set<T> {
parent?: PSet<T>;
static createChild<T>(parent: PSet<T>): PSet<T> {
const pset: PSet<T> = new PSet();
pset.parent = parent;
return pset;
}
has(key: T): boolean {
if (super.has(key)) {
return true;
}
return this.parent ? this.parent.has(key) : false;
}
*[Symbol.iterator](): Generator<T> {
let iterator = super[Symbol.iterator]();
for (let elem of iterator) {
yield elem;
}
if (this.parent) {
for (let elem of this.parent) {
yield elem;
}
}
}
}
// -----------------------------------------------------------------------------
export function observe<T>(value: T, cb: CB): T {
if (isNotObservable(value)) {
return value;
}
if (observers.has(value)) {
const callbacks = observers.get(value)!;
callbacks.add(cb);
return value;
}
const callbacks: PSet<CB> = new PSet();
callbacks.add(cb);
return observeValue(value, callbacks);
}
export function unobserve<T>(value: T, cb: () => void) {
if (isNotObservable(value)) {
return;
}
if (observers.has(value)) {
const callbacks = observers.get(value)!;
callbacks.delete(cb);
}
}
function isNotObservable(value: any): boolean {
return ( return (
typeof value === "object" && value === null || typeof value !== "object" || value instanceof Date || value instanceof Promise
value !== null &&
!(value instanceof Date) &&
!(value instanceof Promise) &&
!(value instanceof String) &&
!(value instanceof Number)
); );
} }
const targetToKeysToCallbacks = new WeakMap<Target, Map<ObjectKey, Set<Callback>>>();
/** /**
* Observes a given key on a target with an callback. The callback will be * value should
* called when the given key changes on the target. * 1. be observable
* * 2. not yet be observed
* @param target the target whose key should be observed
* @param key the key to observe (or Symbol(KEYCHANGES) for key creation
* or deletion)
* @param callback the function to call when the key changes
*/ */
function observeTargetKey(target: Target, key: ObjectKey, callback: Callback): void { function observeValue(value: any, callbacks: PSet<CB>): any {
if (!targetToKeysToCallbacks.get(target)) { const proxy = new Proxy(value as any, {
targetToKeysToCallbacks.set(target, new Map()); get(target: any, key: any): any {
} const current = target[key];
const keyToCallbacks = targetToKeysToCallbacks.get(target)!; if (isNotObservable(current)) {
if (!keyToCallbacks.get(key)) { return current;
keyToCallbacks.set(key, new Set()); }
} if (observers.has(current)) {
keyToCallbacks.get(key)!.add(callback); // this is wrong ?
if (!callbacksToTargets.has(callback)) { observers.get(current)!.parent = callbacks;
callbacksToTargets.set(callback, new Set()); return current;
} }
callbacksToTargets.get(callback)!.add(target); const subCallbacks = PSet.createChild(callbacks);
} const subValue = observeValue(current, subCallbacks);
/** target[key] = subValue;
* Notify Reactives that are observing a given target that a key has changed on return subValue;
* the target. },
* set(target: any, key: any, value: any): boolean {
* @param target target whose Reactives should be notified that the target was // TODO: check if current !== target or proxy ??
* changed. const current = target[key];
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created if (current !== value) {
* or deleted) if (isNotObservable(value)) {
*/ target[key] = value;
function notifyReactives(target: Target, key: ObjectKey): void { } else {
const keyToCallbacks = targetToKeysToCallbacks.get(target); // TODO: test following scenario:
if (!keyToCallbacks) { // 1. obj1 = observer({a:1}, somecb);
return; // 2. unobserve(obj1, somecb)
} // 3. obj1.a = {b: 2};
const callbacks = keyToCallbacks.get(key); // check that somecb was not called
if (!callbacks) { // obj1.a.b = 3;
return; // check again that somecb was not called
} if (observers.has(value)) {
// Loop on copy because clearReactivesForCallback will modify the set in place const pset = observers.get(value)!;
for (const callback of [...callbacks]) { pset.parent = callbacks;
clearReactivesForCallback(callback); target[key] = value;
callback(); } else {
} const subCallbacks = PSet.createChild(callbacks);
target[key] = observeValue(value, subCallbacks);
}
}
notify(target);
}
return true;
},
deleteProperty(target: any, key: any) {
if (key in target) {
delete target[key];
notify(target);
}
return true;
},
});
observers.set(value, callbacks);
observers.set(proxy, callbacks);
return proxy;
} }
const callbacksToTargets = new WeakMap<Callback, Set<Target>>(); function notify(value: any) {
/** const cbs = observers.get(value)!;
* Clears all subscriptions of the Reactives associated with a given callback. for (let cb of cbs) {
* cb();
* @param callback the callback for which the reactives need to be cleared
*/
function clearReactivesForCallback(callback: Callback): void {
const targetsToClear = callbacksToTargets.get(callback);
if (!targetsToClear) {
return;
} }
for (const target of targetsToClear) {
const observedKeys = targetToKeysToCallbacks.get(target);
if (!observedKeys) {
continue;
}
for (const callbacks of observedKeys.values()) {
callbacks.delete(callback);
}
}
targetsToClear.clear();
}
const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive>>();
/**
* Creates a reactive proxy for an object. Reading data on the reactive object
* subscribes to changes to the data. Writing data on the object will cause the
* notify callback to be called if there are suscriptions to that data. Nested
* objects and arrays are automatically made reactive as well.
*
* Whenever you are notified of a change, all subscriptions are cleared, and if
* you would like to be notified of any further changes, you should go read
* the underlying data again. We assume that if you don't go read it again after
* being notified, it means that you are no longer interested in that data.
*
* Subscriptions:
* + Reading a property on an object will subscribe you to changes in the value
* of that property.
* + Accessing an object keys (eg with Object.keys or with `for..in`) will
* subscribe you to the creation/deletion of keys. Checking the presence of a
* key on the object with 'in' has the same effect.
* - getOwnPropertyDescriptor does not currently subscribe you to the property.
* This is a choice that was made because changing a key's value will trigger
* this trap and we do not want to subscribe by writes. This also means that
* Object.hasOwnProperty doesn't subscribe as it goes through this trap.
*
* @param target the object for which to create a reactive proxy
* @param callback the function to call when an observed property of the
* reactive has changed
* @returns a proxy that tracks changes to it
*/
export function reactive<T extends Target>(target: T, callback: Callback = () => {}): Reactive<T> {
if (!canBeMadeReactive(target)) {
throw new Error(`Cannot make the given value reactive`);
}
const originalTarget = (target as Reactive)[TARGET];
if (originalTarget) {
return reactive(originalTarget, callback);
}
if (!reactiveCache.has(target)) {
reactiveCache.set(target, new Map());
}
const reactivesForTarget = reactiveCache.get(target)!;
if (!reactivesForTarget.has(callback)) {
const proxy = new Proxy(target, {
get(target: any, key: ObjectKey, proxy: Reactive<T>) {
if (key === TARGET) {
return target;
}
observeTargetKey(target, key, callback);
const value = Reflect.get(target, key, proxy);
if (!canBeMadeReactive(value)) {
return value;
}
return reactive(value, callback);
},
set(target, key, value, proxy) {
const isNewKey = !Object.hasOwnProperty.call(target, key);
const originalValue = Reflect.get(target, key, proxy);
const ret = Reflect.set(target, key, value, proxy);
if (isNewKey) {
notifyReactives(target, KEYCHANGES);
}
// While Array length may trigger the set trap, it's not actually set by this
// method but is updated behind the scenes, and the trap is not called with the
// new value. We disable the "same-value-optimization" for it because of that.
if (originalValue !== value || (Array.isArray(target) && key === "length")) {
notifyReactives(target, key);
}
return ret;
},
deleteProperty(target, key) {
const ret = Reflect.deleteProperty(target, key);
notifyReactives(target, KEYCHANGES);
notifyReactives(target, key);
return ret;
},
ownKeys(target) {
observeTargetKey(target, KEYCHANGES, callback);
return Reflect.ownKeys(target);
},
has(target, key) {
// TODO: this observes all key changes instead of only the presence of the argument key
observeTargetKey(target, KEYCHANGES, callback);
return Reflect.has(target, key);
},
});
reactivesForTarget.set(callback, proxy);
}
return reactivesForTarget.get(callback) as Reactive<T>;
}
/**
* Creates a batched version of a callback so that all calls to it in the same
* microtick will only call the original callback once.
*
* @param callback the callback to batch
* @returns a batched version of the original callback
*/
export function batched(callback: Callback): Callback {
let called = false;
return async () => {
// This await blocks all calls to the callback here, then releases them sequentially
// in the next microtick. This line decides the granularity of the batch.
await Promise.resolve();
if (!called) {
called = true;
callback();
// wait for all calls in this microtick to fall through before resetting "called"
// so that only the first call to the batched function calls the original callback
await Promise.resolve();
called = false;
}
};
}
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
/**
* Creates a reactive object that will be observed by the current component.
* Reading data from the returned object (eg during rendering) will cause the
* component to subscribe to that data and be rerendered when it changes.
*
* @param state the state to observe
* @returns a reactive object that will cause the component to re-render on
* relevant changes
* @see reactive
*/
export function useState<T extends object>(state: T): Reactive<T> {
const node = getCurrent()!;
if (!batchedRenderFunctions.has(node)) {
batchedRenderFunctions.set(
node,
batched(() => node.render())
);
onWillUnmount(() => clearReactivesForCallback(render));
}
const render = batchedRenderFunctions.get(node)!;
const reactiveState = reactive(state, render);
return reactiveState;
} }
+36
View File
@@ -0,0 +1,36 @@
// -----------------------------------------------------------------------------
// useRef
// -----------------------------------------------------------------------------
import type { Component } from "./component/component";
import { getCurrent } from "./component/component_node";
/**
* The purpose of this hook is to allow components to get a reference to a sub
* html node or component.
*/
interface Ref<C extends Component = Component> {
el: HTMLElement | null;
comp: C | null;
}
export function useRef<C extends Component = Component>(name: string): Ref<C> {
const node = getCurrent()!;
return {
get el(): HTMLElement | null {
const val = node.refs[name];
return val!;
// if (val instanceof HTMLElement) {
// return val;
// } else if (val instanceof Component) {
// return val.el;
// }
// return null;
},
get comp(): C | null {
return null;
// const val = node.refs && node.refs[name];
// return val instanceof Component ? (val as C) : null;
},
};
}
+14
View File
@@ -0,0 +1,14 @@
import { globalTemplates } from "./qweb/template_helpers";
// -----------------------------------------------------------------------------
// Global templates
// -----------------------------------------------------------------------------
export function xml(strings: TemplateStringsArray, ...args: any[]) {
const name = `__template__${xml.nextId++}`;
const value = String.raw(strings, ...args);
globalTemplates[name] = value;
return name;
}
xml.nextId = 1;
-38
View File
@@ -1,38 +0,0 @@
export class EventBus extends EventTarget {
trigger(name: string, payload?: any) {
this.dispatchEvent(new CustomEvent(name, { detail: payload }));
}
}
export function whenReady(fn?: any): Promise<void> {
return new Promise(function (resolve) {
if (document.readyState !== "loading") {
resolve(true);
} else {
document.addEventListener("DOMContentLoaded", resolve, false);
}
}).then(fn || function () {});
}
export async function loadFile(url: string): Promise<string> {
const result = await fetch(url);
if (!result.ok) {
throw new Error("Error while fetching xml templates");
}
return await result.text();
}
/*
* This class just transports the fact that a string is safe
* to be injected as HTML. Overriding a JS primitive is quite painful though
* so we need to redfine toString and valueOf.
*/
export class Markup extends String {}
/*
* Marks a value as safe, that is, a value that can be injected as HTML directly.
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
*/
export function markup(value: any) {
return new Markup(value);
}
-384
View File
@@ -1,384 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Reactivity: useState concurrent renderings 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<span><block-text-0/><block-text-1/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['context'][ctx['props'].key].n;
let d2 = ctx['state'].x;
return block1([d1, d2]);
}
}"
`;
exports[`Reactivity: useState concurrent renderings 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentC\`, {key: ctx['props'].key}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState concurrent renderings 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentB\`, {key: ctx['context'].key}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState destroyed component before being mounted is inactive 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2;
if (ctx['state'].flag) {
b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState destroyed component before being mounted is inactive 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].a;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState destroyed component is inactive 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2;
if (ctx['state'].flag) {
b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState destroyed component is inactive 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].a;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState one components can subscribe twice to same context 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/><block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj1'].a;
let txt2 = ctx['contextObj2'].b;
return block1([txt1, txt2]);
}
}"
`;
exports[`Reactivity: useState parent and children subscribed to same context 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
let txt1 = ctx['contextObj'].b;
return block1([txt1], [b2]);
}
}"
`;
exports[`Reactivity: useState parent and children subscribed to same context 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].a;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState several nodes on different level use same context 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-text-0/> <block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].a;
let d2 = ctx['contextObj'].b;
return block1([d1, d2]);
}
}"
`;
exports[`Reactivity: useState several nodes on different level use same context 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].b;
return block1([d1]);
}
}"
`;
exports[`Reactivity: useState several nodes on different level use same context 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-text-0/><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].a;
let b2 = component(\`L3A\`, {}, key + \`__1\`, node, ctx);
return block1([d1], [b2]);
}
}"
`;
exports[`Reactivity: useState several nodes on different level use same context 4`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`L2A\`, {}, key + \`__1\`, node, ctx);
let b3 = component(\`L2B\`, {}, key + \`__2\`, node, ctx);
return block1([], [b2, b3]);
}
}"
`;
exports[`Reactivity: useState two components are updated in parallel 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
let b3 = component(\`Child\`, {}, key + \`__2\`, node, ctx);
return block1([], [b2, b3]);
}
}"
`;
exports[`Reactivity: useState two components are updated in parallel 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].value;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState two components can subscribe to same context 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
let b3 = component(\`Child\`, {}, key + \`__2\`, node, ctx);
return block1([], [b2, b3]);
}
}"
`;
exports[`Reactivity: useState two components can subscribe to same context 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].value;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState two independent components on different levels are updated in parallel 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
let b3 = component(\`Parent\`, {}, key + \`__2\`, node, ctx);
return block1([], [b2, b3]);
}
}"
`;
exports[`Reactivity: useState two independent components on different levels are updated in parallel 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].value;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState two independent components on different levels are updated in parallel 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState useContext=useState hook is reactive, for one component 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].value;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState useless atoms should be deleted 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, withKey } = helpers;
let block1 = createBlock(\`<div><block-child-0/> Total: <block-text-0/> Count: <block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(Object.keys(ctx['state']));
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`id\`] = v_block2[i1];
let key1 = ctx['id'];
c_block2[i1] = withKey(component(\`Quantity\`, {id: ctx['id']}, key + \`__1__\${key1}\`, node, ctx), key1);
}
ctx = ctx.__proto__;
let b2 = list(c_block2);
let txt1 = ctx['total'];
let txt2 = Object.keys(ctx['state']).length;
return block1([txt1, txt2], [b2]);
}
}"
`;
exports[`Reactivity: useState useless atoms should be deleted 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['state'].quantity;
return block1([txt1]);
}
}"
`;
exports[`Reactivity: useState very simple use, with initial value 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['contextObj'].value;
return block1([txt1]);
}
}"
`;
-43
View File
@@ -1,43 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app App supports env with getters/setters 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/> <block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['env'].someVal;
let txt2 = Object.keys(ctx['env'].services);
return block1([txt1, txt2]);
}
}"
`;
exports[`app can configure an app with props 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['props'].value;
return block1([txt1]);
}
}"
`;
exports[`app destroy remove the widget from the DOM 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
-62
View File
@@ -1,62 +0,0 @@
import { App, Component, xml } from "../../src";
import { status } from "../../src/component/status";
import { makeTestFixture, snapshotEverything, nextTick, elem } from "../helpers";
let fixture: HTMLElement;
snapshotEverything();
beforeEach(() => {
fixture = makeTestFixture();
});
describe("app", () => {
test("destroy remove the widget from the DOM", async () => {
class SomeComponent extends Component {
static template = xml`<div/>`;
}
const app = new App(SomeComponent);
const comp = await app.mount(fixture);
const el = elem(comp);
expect(document.contains(el)).toBe(true);
app.destroy();
expect(document.contains(el)).toBe(false);
expect(status(comp)).toBe("destroyed");
});
test("App supports env with getters/setters", async () => {
let someVal = "maggot";
const services: any = { serv1: "" };
const env = {
get someVal() {
return someVal;
},
services,
};
class SomeComponent extends Component {
static template = xml`<div><t t-esc="env.someVal" /> <t t-esc="Object.keys(env.services)" /></div>`;
}
const app = new App(SomeComponent, { env });
const comp = await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>maggot serv1</div>");
someVal = "brain";
services.serv2 = "";
comp.render();
await nextTick();
expect(fixture.innerHTML).toBe("<div>brain serv1,serv2</div>");
});
test("can configure an app with props", async () => {
class SomeComponent extends Component {
static template = xml`<div t-esc="props.value"/>`;
}
const app = new App(SomeComponent, { props: { value: 333 } });
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>333</div>");
});
});
-32
View File
@@ -227,38 +227,6 @@ describe("misc", () => {
expect(fixture.innerHTML).toBe("<p>() =&gt; 3tostring</p>"); expect(fixture.innerHTML).toBe("<p>() =&gt; 3tostring</p>");
}); });
test("block with 2 subblocks: variation", async () => {
const block = createBlock("<a><b><c><block-child-0/>2</c></b><block-child-1/></a>");
const tree = block([], [text("1"), text("3")]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<a><b><c>12</c></b>3</a>");
});
test("block with 2 subblocks: another variation", async () => {
const block = createBlock(
`<a block-attribute-0="hello"><b><c><block-child-0/>2</c></b><block-child-1/></a>`
);
const tree = block(["world"], [text("1"), text("3")]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<a hello="world"><b><c>12</c></b>3</a>`);
});
test("namespace is not propagated to siblings", () => {
const block = createBlock(`<div><svg block-ns="someNameSpace"><g/></svg><div></div></div>`);
const fixture = makeTestFixture();
mount(block(), fixture);
expect(fixture.innerHTML).toBe("<div><svg><g></g></svg><div></div></div>");
expect(fixture.querySelector("svg")!.namespaceURI).toBe("someNameSpace");
expect(fixture.querySelector("g")!.namespaceURI).toBe("someNameSpace");
const allDivs = fixture.querySelectorAll("div");
expect(Array.from(allDivs).map((el) => el.namespaceURI)).toEqual([
"http://www.w3.org/1999/xhtml",
"http://www.w3.org/1999/xhtml",
]);
});
// test.skip("reusing a block skips patching process", async () => { // test.skip("reusing a block skips patching process", async () => {
// const block = createBlock('<div><block-text-0/></div>'); // const block = createBlock('<div><block-text-0/></div>');
// const foo = block(["foo"]); // const foo = block(["foo"]);
-59
View File
@@ -26,26 +26,6 @@ test("simple attribute", async () => {
expect(fixture.innerHTML).toBe(`<div hello="owl"></div>`); expect(fixture.innerHTML).toBe(`<div hello="owl"></div>`);
}); });
test("updating attribute with falsy value", async () => {
const block = createBlock('<div block-attribute-0="hello"></div>');
const tree = block([false]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div></div>`);
patch(tree, block(["owl"]));
expect(fixture.innerHTML).toBe(`<div hello="owl"></div>`);
patch(tree, block([false]));
expect(fixture.innerHTML).toBe(`<div></div>`);
patch(tree, block(["owl"]));
expect(fixture.innerHTML).toBe(`<div hello="owl"></div>`);
patch(tree, block([undefined]));
expect(fixture.innerHTML).toBe(`<div></div>`);
});
test("dynamic attribute (pair)", async () => { test("dynamic attribute (pair)", async () => {
const block = createBlock('<div block-attributes="0"></div>'); const block = createBlock('<div block-attributes="0"></div>');
const tree = block([["hello", "world"]]); const tree = block([["hello", "world"]]);
@@ -57,20 +37,6 @@ test("dynamic attribute (pair)", async () => {
expect(fixture.innerHTML).toBe(`<div ola="mundo"></div>`); expect(fixture.innerHTML).toBe(`<div ola="mundo"></div>`);
}); });
test("dynamic attribute (pair, with false value)", async () => {
const block = createBlock('<div block-attributes="0"></div>');
const tree = block([["hello", false]]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div></div>`);
patch(tree, block([["hello", "world"]]));
expect(fixture.innerHTML).toBe(`<div hello="world"></div>`);
patch(tree, block([["hello", false]]));
expect(fixture.innerHTML).toBe(`<div></div>`);
});
test("dynamic attribute (object)", async () => { test("dynamic attribute (object)", async () => {
const block = createBlock('<div block-attributes="0"></div>'); const block = createBlock('<div block-attributes="0"></div>');
const tree = block([{ hello: "world" }]); const tree = block([{ hello: "world" }]);
@@ -82,23 +48,6 @@ test("dynamic attribute (object)", async () => {
expect(fixture.innerHTML).toBe(`<div ola="mundo"></div>`); expect(fixture.innerHTML).toBe(`<div ola="mundo"></div>`);
}); });
test("dynamic attribute (object), with falsy values", async () => {
const block = createBlock('<div block-attributes="0"></div>');
const tree = block([{ hello: "world", blip: false }]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div hello="world"></div>`);
patch(tree, block([{ ola: "mundo", blip: undefined }]));
expect(fixture.innerHTML).toBe(`<div ola="mundo"></div>`);
patch(tree, block([{ ola: false, blip: 1 }]));
expect(fixture.innerHTML).toBe(`<div blip="1"></div>`);
patch(tree, block([{ ola: undefined, blip: undefined }]));
expect(fixture.innerHTML).toBe(`<div></div>`);
});
test("class attribute", async () => { test("class attribute", async () => {
const block = createBlock('<div block-attribute-0="class"></div>'); const block = createBlock('<div block-attribute-0="class"></div>');
const tree = block(["fire"]); const tree = block(["fire"]);
@@ -116,14 +65,6 @@ test("class attribute", async () => {
expect(fixture.innerHTML).toBe(`<div class="0"></div>`); expect(fixture.innerHTML).toBe(`<div class="0"></div>`);
}); });
test("attribute with undefined value", async () => {
const block = createBlock('<div block-attribute-0="abc"></div>');
const tree = block([undefined]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div></div>`);
});
test("class attribute with undefined value", async () => { test("class attribute with undefined value", async () => {
const block = createBlock('<div block-attribute-0="class"></div>'); const block = createBlock('<div block-attribute-0="class"></div>');
const tree = block([undefined]); const tree = block([undefined]);
-165
View File
@@ -61,7 +61,6 @@ test("simple event handling ", async () => {
const [owner, method] = data; const [owner, method] = data;
owner[method](); owner[method]();
} }
return false;
}; };
const block = createBlock('<div block-handler-0="click"></div>'); const block = createBlock('<div block-handler-0="click"></div>');
@@ -107,167 +106,3 @@ test("two same block nodes with different handlers", async () => {
(fixture.firstChild!.nextSibling as HTMLDivElement).click(); (fixture.firstChild!.nextSibling as HTMLDivElement).click();
expect(steps).toEqual(["1", "2"]); expect(steps).toEqual(["1", "2"]);
}); });
test("two same block nodes with different handlers (synthetic)", async () => {
const block = createBlock('<div block-handler-0="click.synthetic"></div>');
let steps: string[] = [];
let handler1 = () => steps.push("1");
let handler2 = () => steps.push("2");
const tree = multi([block([handler1]), block([handler2])]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div><div></div>");
(fixture.firstChild as HTMLDivElement).click();
(fixture.firstChild!.nextSibling as HTMLDivElement).click();
expect(steps).toEqual(["1", "2"]);
});
test("two event handlers on same event", async () => {
const block = createBlock('<div block-handler-0="click" block-handler-1="click"></div>');
let n = 0;
let m = 0;
const tree = block([() => m++, () => n++]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
expect(m).toBe(0);
expect(n).toBe(0);
(fixture.firstChild as HTMLDivElement).click();
expect(m).toBe(1);
expect(n).toBe(1);
});
test("two synthetic event handlers on same event", async () => {
const block = createBlock(
'<div block-handler-0="click.synthetic" block-handler-1="click.synthetic"></div>'
);
let n = 0;
let m = 0;
const tree = block([() => m++, () => n++]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
expect(m).toBe(0);
expect(n).toBe(0);
(fixture.firstChild as HTMLDivElement).click();
expect(m).toBe(1);
expect(n).toBe(1);
});
test("synthetic and native handlers can cohabitate", async () => {
const block = createBlock(
'<div block-handler-0="click.synthetic"><div block-handler-1="click"/></div>'
);
let steps: string[] = [];
let handler1 = () => steps.push("1");
let handler2 = () => steps.push("2");
const tree = block([handler1, handler2]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><div></div></div>");
(fixture.firstChild!.firstChild as HTMLDivElement).click();
expect(steps).toEqual(["2", "1"]);
(fixture.firstChild as HTMLDivElement).click();
expect(steps).toEqual(["2", "1", "1"]);
});
test("synthetic and native handlers can cohabitate (2)", async () => {
const block = createBlock(
'<div block-handler-0="click"><div block-handler-1="click.synthetic"/></div>'
);
let steps: string[] = [];
let handler1 = () => steps.push("1");
let handler2 = () => steps.push("2");
const tree = block([handler1, handler2]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><div></div></div>");
(fixture.firstChild!.firstChild as HTMLDivElement).click();
expect(steps).toEqual(["1", "2"]);
(fixture.firstChild as HTMLDivElement).click();
expect(steps).toEqual(["1", "2", "1"]);
});
test("synthetic and native handlers can cohabitate (3)", async () => {
const parent = createBlock(`<div block-handler-0="click"><block-child-0/><block-child-1/></div>`);
const block = createBlock('<div block-handler-0="click"/>');
const blockSynth = createBlock('<div block-handler-0="click.synthetic"/>');
let steps: string[] = [];
const handler0 = () => steps.push("0");
let handler1 = () => steps.push("1");
let handler2 = () => steps.push("2");
const tree = parent([handler0], [block([handler1]), blockSynth([handler2])]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><div></div><div></div></div>");
const children = fixture.children[0].children;
(children[0] as HTMLElement).click();
expect(steps).toEqual(["1", "0"]);
(children[1] as HTMLElement).click();
expect(steps).toEqual(["1", "0", "0", "2"]);
});
test("synthetic and native handlers can cohabitate (4)", async () => {
const parent = createBlock(`<div block-handler-0="click"><block-child-0/><block-child-1/></div>`);
const block = createBlock('<div block-handler-0="click"/>');
const blockSynth = createBlock('<div block-handler-0="click.synthetic"/>');
let steps: string[] = [];
const handler0 = (ev: Event) => {
steps.push("0");
ev.stopPropagation();
};
let handler1 = () => steps.push("1");
let handler2 = () => steps.push("2");
const tree = parent([handler0], [block([handler1]), blockSynth([handler2])]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div><div></div><div></div></div>");
const children = fixture.children[0].children;
(children[0] as HTMLElement).click();
expect(steps).toEqual(["1", "0"]);
(children[1] as HTMLElement).click();
expect(steps).toEqual(["1", "0", "0"]);
});
test("synthetic and native handlers can cohabitate (5)", async () => {
const block = createBlock('<div block-handler-0="click.synthetic" block-handler-1="click"/>');
let steps: string[] = [];
let handler1 = () => steps.push("1");
let handler2 = () => steps.push("2");
const tree = block([handler1, handler2]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
(fixture.firstChild as HTMLDivElement).click();
expect(steps).toEqual(["2", "1"]);
});
test("synthetic and native handlers can cohabitate (6)", async () => {
const block = createBlock(`<div
block-handler-0="click.synthetic"
block-handler-1="click"
block-handler-2="click.synthetic"
block-handler-3="click"
/>`);
let steps: string[] = [];
const handler1 = () => steps.push("1");
const handler2 = () => steps.push("2");
const handler3 = () => steps.push("3");
const handler4 = () => steps.push("4");
const tree = block([handler1, handler2, handler3, handler4]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
(fixture.firstChild as HTMLDivElement).click();
expect(steps).toEqual(["2", "4", "1", "3"]);
});
-26
View File
@@ -1,26 +0,0 @@
import { comment, mount } from "../../src/blockdom";
import { makeTestFixture } from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
test("simple comment node", async () => {
const tree = comment("foo");
expect(tree.el).toBe(undefined);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<!--foo-->");
expect(tree.el).not.toBe(undefined);
tree.remove();
expect(fixture.innerHTML).toBe("");
});
-7
View File
@@ -76,11 +76,4 @@ describe("multi blocks", () => {
mount(text(multi([text("a"), text("b")]) as any), fixture); mount(text(multi([text("a"), text("b")]) as any), fixture);
expect(fixture.innerHTML).toBe("ab"); expect(fixture.innerHTML).toBe("ab");
}); });
test("multi inside a block", async () => {
const block = createBlock("<div><block-child-0/></div>");
const tree = block([], [multi([text("foo"), text("bar")])]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<div>foobar</div>");
});
}); });
-72
View File
@@ -1,72 +0,0 @@
import { createBlock, mount } from "../../src/blockdom";
import { makeTestFixture } from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
const XHTML_URI = "http://www.w3.org/1999/xhtml";
const SVG_URI = "http://www.w3.org/2000/svg";
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("namespace", () => {
test("default namespace is xhtml", () => {
const block = createBlock(`<tag/>`);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<tag></tag>");
expect(fixture.firstElementChild!.namespaceURI).toBe(XHTML_URI);
});
test("namespace can be changed with block-ns", () => {
const block = createBlock(`<tag block-ns="${SVG_URI}"/>`);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<tag></tag>");
expect(fixture.firstElementChild!.namespaceURI).toBe(SVG_URI);
});
test("namespace is kept for children", () => {
const block = createBlock(
`<parent block-ns="${SVG_URI}"><child><subchild/></child><child/></parent>`
);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe(
"<parent><child><subchild></subchild></child><child></child></parent>"
);
const parent = fixture.firstElementChild!;
const child1 = parent.firstElementChild!;
const subchild = child1.firstElementChild!;
const child2 = child1.nextElementSibling!;
expect(parent.namespaceURI).toBe(SVG_URI);
expect(child1.namespaceURI).toBe(SVG_URI);
expect(child2.namespaceURI).toBe(SVG_URI);
expect(subchild.namespaceURI).toBe(SVG_URI);
});
test("various namespaces in same block", () => {
const block = createBlock(`<none><one block-ns="one"/><two block-ns="two"/></none>`);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<none><one></one><two></two></none>");
const none = fixture.firstElementChild!;
const one = none.firstElementChild!;
const two = one.nextElementSibling!;
expect(none.namespaceURI).toBe(XHTML_URI);
expect(one.namespaceURI).toBe("one");
expect(two.namespaceURI).toBe("two");
});
});
-57
View File
@@ -54,61 +54,4 @@ describe("togglers", () => {
expect(fixture.innerHTML).toBe("<p>hey</p>"); expect(fixture.innerHTML).toBe("<p>hey</p>");
expect(fixture.firstChild).not.toBe(textnode); expect(fixture.firstChild).not.toBe(textnode);
}); });
test("beforeRemove is called", () => {
const block = createBlock("<p>hey</p>");
function blockOverriden() {
const vn = block();
vn.beforeRemove = () => {
steps.push("beforeRemove");
};
return vn;
}
const steps: string[] = [];
const tree = toggler("key1", blockOverriden());
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<p>hey</p>");
patch(tree, toggler("key2", text("foo")));
expect(fixture.innerHTML).toBe("foo");
patch(tree, toggler("key3", blockOverriden()), true);
expect(fixture.innerHTML).toBe("<p>hey</p>");
patch(tree, toggler("key2", text("foo")), true);
expect(fixture.innerHTML).toBe("foo");
expect(steps).toEqual(["beforeRemove"]);
});
test("beforeRemove is called when within a block", () => {
const block = createBlock("<p><block-child-0/></p>");
const conditionalBlock = createBlock("<p>hey</p>");
const steps: string[] = [];
function blockOverriden() {
const vn = conditionalBlock();
vn.beforeRemove = function () {
steps.push("beforeRemove");
};
return vn;
}
const mainBlock = block([], [toggler("key1", blockOverriden())]);
mount(mainBlock, fixture);
expect(fixture.innerHTML).toBe("<p><p>hey</p></p>");
patch(mainBlock, block([], []));
expect(fixture.innerHTML).toBe("<p></p>");
patch(mainBlock, block([], [toggler("key1", blockOverriden())]), true);
expect(fixture.innerHTML).toBe("<p><p>hey</p></p>");
patch(mainBlock, block([], []), true);
expect(fixture.innerHTML).toBe("<p></p>");
expect(steps).toEqual(["beforeRemove"]);
});
}); });
@@ -1,530 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`t-on can bind event handler 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['add'], ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on can bind handlers with arguments 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['add'];
let hdlr1 = [()=>v1(5), ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on can bind handlers with empty object 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['doSomething'];
let hdlr1 = [()=>v1({}), ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on can bind handlers with empty object (with non empty inner string) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['doSomething'];
let hdlr1 = [()=>v1({}), ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on can bind handlers with empty object (with non empty inner string) 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, withKey } = helpers;
let block1 = createBlock(\`<ul><block-child-0/></ul>\`);
let block3 = createBlock(\`<li><a block-handler-0=\\"click\\">link</a></li>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(['someval']);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`action\`] = v_block2[i1];
ctx[\`action_index\`] = i1;
let key1 = ctx['action_index'];
const v1 = ctx['activate'];
const v2 = ctx['action'];
let hdlr1 = [()=>v1(v2), ctx];
c_block2[i1] = withKey(block3([hdlr1]), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
}
}"
`;
exports[`t-on can bind handlers with object arguments 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['add'];
let hdlr1 = [()=>v1({val:5}), ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on can bind two event handlers 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\" block-handler-1=\\"dblclick\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['handleClick'], ctx];
let hdlr2 = [ctx['handleDblClick'], ctx];
return block1([hdlr1, hdlr2]);
}
}"
`;
exports[`t-on handler is bound to proper owner 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['add'], ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on handler is bound to proper owner, part 2 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, withKey } = helpers;
let block2 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList([1]);
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`value\`] = v_block1[i1];
let key1 = ctx['value'];
let hdlr1 = [ctx['add'], ctx];
c_block1[i1] = withKey(block2([hdlr1]), key1);
}
return list(c_block1);
}
}"
`;
exports[`t-on handler is bound to proper owner, part 3 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`sub\`);
return function template(ctx, node, key = \\"\\") {
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
}
}"
`;
exports[`t-on handler is bound to proper owner, part 3 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['add'], ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on handler is bound to proper owner, part 4 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, getTemplate, withKey } = helpers;
const callTemplate_1 = getTemplate(\`sub\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList([1]);
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`value\`] = v_block1[i1];
ctx[\`value_first\`] = i1 === 0;
ctx[\`value_last\`] = i1 === v_block1.length - 1;
ctx[\`value_index\`] = i1;
ctx[\`value_value\`] = k_block1[i1];
let key1 = ctx['value'];
c_block1[i1] = withKey(callTemplate_1.call(this, ctx, node, key + \`__1__\${key1}\`), key1);
}
return list(c_block1);
}
}"
`;
exports[`t-on handler is bound to proper owner, part 4 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['add'], ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on receive event in first argument 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['add'], ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) basic support for native listener 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div class=\\"myClass\\" block-handler-0=\\"click\\"><button block-handler-1=\\"click\\">Button</button></div>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['divClicked'], ctx];
let hdlr2 = [ctx['btnClicked'], ctx];
return block1([hdlr1, hdlr2]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on combined with t-esc 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><button block-handler-0=\\"click\\"><block-text-1/></button></div>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['onClick'], ctx];
let txt1 = ctx['text'];
return block1([hdlr1, txt1]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on combined with t-out 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<div><button block-handler-0=\\"click\\"><block-child-0/></button></div>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['onClick'], ctx];
let b2 = safeOutput(ctx['html']);
return block1([hdlr1], [b2]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on with .capture modifier 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div block-handler-0=\\"click.capture\\"><button block-handler-1=\\"click\\">Button</button></div>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [\\"capture\\", ctx['onCapture'], ctx];
let hdlr2 = [ctx['doSomething'], ctx];
return block1([hdlr1, hdlr2]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on with empty handler (only modifiers) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><button block-handler-0=\\"click.prevent\\">Button</button></div>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [\\"prevent\\", , ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on with prevent and self modifiers (order matters) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><button block-handler-0=\\"click.prevent.self\\"><span>Button</span></button></div>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [\\"prevent\\",\\"self\\", ctx['onClick'], ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on with prevent and/or stop modifiers 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><button block-handler-0=\\"click.prevent\\">Button 1</button><button block-handler-1=\\"click.stop\\">Button 2</button><button block-handler-2=\\"click.prevent.stop\\">Button 3</button></div>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [\\"prevent\\", ctx['onClickPrevented'], ctx];
let hdlr2 = [\\"stop\\", ctx['onClickStopped'], ctx];
let hdlr3 = [\\"prevent\\",\\"stop\\", ctx['onClickPreventedAndStopped'], ctx];
return block1([hdlr1, hdlr2, hdlr3]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on with prevent modifier in t-foreach 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, withKey } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block3 = createBlock(\`<a href=\\"#\\" block-handler-0=\\"click.prevent\\"> Edit <block-text-1/></a>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['projects']);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`project\`] = v_block2[i1];
let key1 = ctx['project'];
const v1 = ctx['onEdit'];
const v2 = ctx['project'];
let hdlr1 = [\\"prevent\\", ev=>v1(v2.id,ev), ctx];
let txt1 = ctx['project'].name;
c_block2[i1] = withKey(block3([hdlr1, txt1]), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on with self and prevent modifiers (order matters) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><button block-handler-0=\\"click.self.prevent\\"><span>Button</span></button></div>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [\\"self\\",\\"prevent\\", ctx['onClick'], ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on t-on modifiers (native listener) t-on with self modifier 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><button block-handler-0=\\"click\\"><span>Button</span></button><button block-handler-1=\\"click.self\\"><span>Button</span></button></div>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['onClick'], ctx];
let hdlr2 = [\\"self\\", ctx['onClickSelf'], ctx];
return block1([hdlr1, hdlr2]);
}
}"
`;
exports[`t-on t-on modifiers (synthetic listener) basic support for synthetic 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div block-handler-0=\\"click.synthetic\\"><button block-handler-1=\\"click.synthetic\\">Button</button></div>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [\\"synthetic\\", ctx['divClicked'], ctx];
let hdlr2 = [\\"synthetic\\", ctx['btnClicked'], ctx];
return block1([hdlr1, hdlr2]);
}
}"
`;
exports[`t-on t-on with inline statement (function call) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['state'];
let hdlr1 = [()=>v1.incrementCounter(2), ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on t-on with inline statement 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['state'];
let hdlr1 = [()=>v1.counter++, ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on t-on with inline statement, part 2 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Toggle</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['state'];
let hdlr1 = [()=>v1.flag=!v1.flag, ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on t-on with inline statement, part 3 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Toggle</button>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['state'];
const v2 = ctx['someFunction'];
let hdlr1 = [()=>v1.n=v2(3), ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on t-on with t-call 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`sub\`);
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
`;
exports[`t-on t-on with t-call 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['update'], ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-on t-on, with arguments and t-call 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`sub\`);
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
`;
exports[`t-on t-on, with arguments and t-call 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`);
return function template(ctx, node, key = \\"\\") {
const v1 = ctx['value'];
let hdlr1 = [()=>this.update(v1), ctx];
return block1([hdlr1]);
}
}"
`;
@@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`qweb parser simple t-foreach expression, t-key mandatory 1`] = `"\\"Directive t-foreach should always be used with a t-key!\\" (expression: t-foreach=\\"list\\" t-as=\\"item\\")"`;
@@ -1,24 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`memory t-foreach does not leak stuff in global scope 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, withKey } = helpers;
let block1 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList([3,2,1]);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`item\`] = v_block2[i1];
ctx[\`item_index\`] = i1;
let key1 = ctx['item_index'];
c_block2[i1] = withKey(text(ctx['item']), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
}
}"
`;
@@ -1,53 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`properly support svg add proper namespace to g tags 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<g block-ns=\\"http://www.w3.org/2000/svg\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/> </g>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`properly support svg add proper namespace to svg 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\" width=\\"100px\\" height=\\"90px\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/> </svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`properly support svg namespace to g tags not added if already in svg namespace 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><g/></svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`properly support svg namespace to svg tags added even if already in svg namespace 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><svg block-ns=\\"http://www.w3.org/2000/svg\\"/></svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
@@ -1,69 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`debugging t-debug 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<span>hey</span>\`);
return function template(ctx, node, key = \\"\\") {
debugger;
let b2;
if (true) {
debugger;
b2 = block2();
}
return block1([], [b2]);
}
}"
`;
exports[`debugging t-debug on sub template 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<p>coucou</p>\`);
return function template(ctx, node, key = \\"\\") {
debugger;
return block1();
}
}"
`;
exports[`debugging t-debug on sub template 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`sub\`);
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
`;
exports[`debugging t-log 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"foo\\", 42);
console.log(ctx['foo']+3);
return block1();
}
}"
`;
@@ -1,70 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`t-key can use t-key directive on a node 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['beer'].id;
let txt1 = ctx['beer'].name;
return toggler(tKey_1, block1([txt1]));
}
}"
`;
exports[`t-key can use t-key directive on a node 2 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['beer'].id;
let txt1 = ctx['beer'].name;
return toggler(tKey_1, block1([txt1]));
}
}"
`;
exports[`t-key can use t-key directive on a node as a function 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['getKey'](ctx['beer']);
let txt1 = ctx['beer'].name;
return toggler(tKey_1, block1([txt1]));
}
}"
`;
exports[`t-key t-key directive in a list 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, withKey } = helpers;
let block1 = createBlock(\`<ul><block-child-0/></ul>\`);
let block3 = createBlock(\`<li><block-text-0/></li>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['beers']);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`beer\`] = v_block2[i1];
let key1 = ctx['beer'].id;
let txt1 = ctx['beer'].name;
c_block2[i1] = withKey(block3([txt1]), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
}
}"
`;
@@ -1,438 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`t-out literal 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput('ok');
return block1([], [b2]);
}
}"
`;
exports[`t-out literal, no outside html element 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return safeOutput('ok');
}
}"
`;
exports[`t-out multiple calls to t-out 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, zero, getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`sub\`);
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<span>coucou</span>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1;
let b2 = block2();
ctx[zero] = b2;
let b3 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
return block1([], [b3]);
}
}"
`;
exports[`t-out multiple calls to t-out 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { zero } = helpers;
let block1 = createBlock(\`<div><block-child-0/><div>Greeter</div><block-child-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = ctx[zero];
let b3 = ctx[zero];
return block1([], [b2, b3]);
}
}"
`;
exports[`t-out not escaping 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-out t-out 0 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, zero, getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`_basic-callee\`);
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<div>zero</div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1;
let b2 = block2();
ctx[zero] = b2;
let b3 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
return block1([], [b3]);
}
}"
`;
exports[`t-out t-out 0 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { zero } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = ctx[zero];
return block1([], [b2]);
}
}"
`;
exports[`t-out t-out and another sibling node 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<span><span>hello</span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-out t-out bdom 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, LazyValue, safeOutput } = helpers;
let block1 = createBlock(\`<div><span><block-child-0/></span></div>\`);
let block2 = createBlock(\`<ol>set</ol>\`);
function value1(ctx, node, key = \\"\\") {
return block2();
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
ctx[\`var\`] = new LazyValue(value1, ctx, node);
let b3 = safeOutput(ctx['var']);
return block1([], [b3]);
}
}"
`;
exports[`t-out t-out block 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['bdom']);
return block1([], [b2]);
}
}"
`;
exports[`t-out t-out escaped 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-out t-out markedup 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-out t-out on a node with a body, as a default 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput, withDefault } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b3 = text(\`nope\`);
let b2 = withDefault(safeOutput(ctx['var']), b3);
return block1([], [b2]);
}
}"
`;
exports[`t-out t-out on a node with a dom node in body, as a default 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput, withDefault } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
let block3 = createBlock(\`<div>nope</div>\`);
return function template(ctx, node, key = \\"\\") {
let b3 = block3();
let b2 = withDefault(safeOutput(ctx['var']), b3);
return block1([], [b2]);
}
}"
`;
exports[`t-out t-out switch escaped 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-out t-out switch escaped on markup 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-out t-out switch markup 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-out t-out switch markup on bdom 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, LazyValue, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
let block2 = createBlock(\`<ol>set</ol>\`);
let block3 = createBlock(\`<span><block-child-0/></span>\`);
let block5 = createBlock(\`<span><block-child-0/></span>\`);
function value1(ctx, node, key = \\"\\") {
return block2();
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
let b3,b5;
ctx[\`bdom\`] = new LazyValue(value1, ctx, node);
if (ctx['hasBdom']) {
let b4 = safeOutput(ctx['bdom']);
b3 = block3([], [b4]);
} else {
let b6 = safeOutput(ctx['var']);
b5 = block5([], [b6]);
}
return block1([], [b3, b5]);
}
}"
`;
exports[`t-out t-out switch markup on escaped 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-out t-out with a <t/> in body 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return safeOutput(ctx['var']);
}
}"
`;
exports[`t-out t-out with arbitrary object 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-out t-out with arbitrary object 2 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-out t-out with comment 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-out t-out with just a t-set t-value in body 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return safeOutput(ctx['var']);
}
}"
`;
exports[`t-out variable 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-raw is deprecated should warn 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-raw is deprecated t-out is actually called in t-raw's place 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
@@ -1,616 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`t-set evaluate value expression 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"value\\", 1+2);
let txt1 = ctx['value'];
return block1([txt1]);
}
}"
`;
exports[`t-set evaluate value expression, part 2 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"value\\", ctx['somevariable']+2);
let txt1 = ctx['value'];
return block1([txt1]);
}
}"
`;
exports[`t-set set from attribute literal (no outside div) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"value\\", 'ok');
return text(ctx['value']);
}
}"
`;
exports[`t-set set from attribute literal 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"value\\", 'ok');
let txt1 = ctx['value'];
return block1([txt1]);
}
}"
`;
exports[`t-set set from attribute lookup 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"stuff\\", ctx['value']);
let txt1 = ctx['stuff'];
return block1([txt1]);
}
}"
`;
exports[`t-set set from body literal 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"value\\", \`ok\`);
return text(ctx['value']);
}
}"
`;
exports[`t-set set from body lookup 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, LazyValue } = helpers;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
function value1(ctx, node, key = \\"\\") {
return text(ctx['value']);
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
ctx[\`stuff\`] = new LazyValue(value1, ctx, node);
let txt1 = ctx['stuff'];
return block1([txt1]);
}
}"
`;
exports[`t-set set from empty body 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"stuff\\", null);
let txt1 = ctx['stuff'];
return block1([txt1]);
}
}"
`;
exports[`t-set t-set and t-if 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
let b2;
setContextValue(ctx, \\"v\\", ctx['value']);
if (ctx['v']==='ok') {
b2 = text(\`grimbergen\`);
}
return block1([], [b2]);
}
}"
`;
exports[`t-set t-set body is evaluated immediately 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue, LazyValue, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<span><block-text-0/></span>\`);
function value1(ctx, node, key = \\"\\") {
let txt1 = ctx['v1'];
return block2([txt1]);
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"v1\\", 'before');
ctx[\`v2\`] = new LazyValue(value1, ctx, node);
setContextValue(ctx, \\"v1\\", 'after');
let b3 = safeOutput(ctx['v2']);
return block1([], [b3]);
}
}"
`;
exports[`t-set t-set can't alter from within callee 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue, getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`sub\`);
let block1 = createBlock(\`<div><p><block-text-0/></p><block-child-0/><p><block-text-1/></p></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"iter\\", 'source');
let txt1 = ctx['iter'];
let b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
let txt2 = ctx['iter'];
return block1([txt1, txt2], [b2]);
}
}"
`;
exports[`t-set t-set can't alter from within callee 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-text-0/><block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
let txt1 = ctx['iter'];
setContextValue(ctx, \\"iter\\", 'called');
let txt2 = ctx['iter'];
return block1([txt1, txt2]);
}
}"
`;
exports[`t-set t-set can't alter in t-call body 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue, getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`sub\`);
let block1 = createBlock(\`<div><p><block-text-0/></p><block-child-0/><p><block-text-1/></p></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"iter\\", 'source');
let txt1 = ctx['iter'];
ctx = Object.create(ctx);
ctx[isBoundary] = 1;
setContextValue(ctx, \\"iter\\", 'inCall');
let b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
ctx = ctx.__proto__;
let txt2 = ctx['iter'];
return block1([txt1, txt2], [b2]);
}
}"
`;
exports[`t-set t-set can't alter in t-call body 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-text-0/><block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
let txt1 = ctx['iter'];
setContextValue(ctx, \\"iter\\", 'called');
let txt2 = ctx['iter'];
return block1([txt1, txt2]);
}
}"
`;
exports[`t-set t-set does not modify render context existing key values 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"value\\", 35);
let txt1 = ctx['value'];
return block1([txt1]);
}
}"
`;
exports[`t-set t-set evaluates an expression only once 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-text-0/><block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"v\\", ctx['value']+' artois');
let txt1 = ctx['v'];
let txt2 = ctx['v'];
return block1([txt1, txt2]);
}
}"
`;
exports[`t-set t-set outside modified in t-foreach 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue, prepareList, withKey } = helpers;
let block1 = createBlock(\`<div><block-child-0/><p>EndLoop: <block-text-0/></p></div>\`);
let block3 = createBlock(\`<p>InLoop: <block-text-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"iter\\", 0);
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(['a','b']);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`val\`] = v_block2[i1];
let key1 = ctx['val'];
let txt1 = ctx['iter'];
c_block2[i1] = withKey(block3([txt1]), key1);
setContextValue(ctx, \\"iter\\", ctx['iter']+1);
}
ctx = ctx.__proto__;
let b2 = list(c_block2);
let txt2 = ctx['iter'];
return block1([txt2], [b2]);
}
}"
`;
exports[`t-set t-set outside modified in t-foreach increment-after operator 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue, prepareList, withKey } = helpers;
let block1 = createBlock(\`<div><block-child-0/><p>EndLoop: <block-text-0/></p></div>\`);
let block3 = createBlock(\`<p>InLoop: <block-text-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"iter\\", 0);
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(['a','b']);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`val\`] = v_block2[i1];
let key1 = ctx['val'];
let txt1 = ctx['iter'];
c_block2[i1] = withKey(block3([txt1]), key1);
setContextValue(ctx, \\"iter\\", ctx['iter']++);
}
ctx = ctx.__proto__;
let b2 = list(c_block2);
let txt2 = ctx['iter'];
return block1([txt2], [b2]);
}
}"
`;
exports[`t-set t-set outside modified in t-foreach increment-before operator 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue, prepareList, withKey } = helpers;
let block1 = createBlock(\`<div><block-child-0/><p>EndLoop: <block-text-0/></p></div>\`);
let block3 = createBlock(\`<p>InLoop: <block-text-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"iter\\", 0);
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(['a','b']);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`val\`] = v_block2[i1];
let key1 = ctx['val'];
let txt1 = ctx['iter'];
c_block2[i1] = withKey(block3([txt1]), key1);
setContextValue(ctx, \\"iter\\", ++ctx['iter']);
}
ctx = ctx.__proto__;
let b2 = list(c_block2);
let txt2 = ctx['iter'];
return block1([txt2], [b2]);
}
}"
`;
exports[`t-set t-set should reuse variable if possible 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue, prepareList, withKey } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block3 = createBlock(\`<div><span>v<block-text-0/></span></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"v\\", 1);
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['list']);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`elem\`] = v_block2[i1];
ctx[\`elem_index\`] = i1;
let key1 = ctx['elem_index'];
let txt1 = ctx['v'];
setContextValue(ctx, \\"v\\", ctx['elem']);
c_block2[i1] = withKey(block3([txt1]), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
}
}"
`;
exports[`t-set t-set with content and sub t-esc 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, LazyValue } = helpers;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
function value1(ctx, node, key = \\"\\") {
let b3 = text(ctx['beep']);
let b4 = text(\` boop\`);
return multi([b3, b4]);
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
ctx[\`setvar\`] = new LazyValue(value1, ctx, node);
let txt1 = ctx['setvar'];
return block1([txt1]);
}
}"
`;
exports[`t-set t-set with t-value (falsy) and body 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue, LazyValue, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<span><block-text-0/></span>\`);
function value1(ctx, node, key = \\"\\") {
let txt1 = ctx['v1'];
return block2([txt1]);
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"v3\\", false);
setContextValue(ctx, \\"v1\\", 'before');
ctx[\`v2\`] = withDefault(ctx['v3'], new LazyValue(value1, ctx, node));
setContextValue(ctx, \\"v1\\", 'after');
setContextValue(ctx, \\"v3\\", true);
let b3 = safeOutput(ctx['v2']);
return block1([], [b3]);
}
}"
`;
exports[`t-set t-set with t-value (truthy) and body 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue, LazyValue, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<span><block-text-0/></span>\`);
function value1(ctx, node, key = \\"\\") {
let txt1 = ctx['v1'];
return block2([txt1]);
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"v3\\", 'Truthy');
setContextValue(ctx, \\"v1\\", 'before');
ctx[\`v2\`] = withDefault(ctx['v3'], new LazyValue(value1, ctx, node));
setContextValue(ctx, \\"v1\\", 'after');
setContextValue(ctx, \\"v3\\", false);
let b3 = safeOutput(ctx['v2']);
return block1([], [b3]);
}
}"
`;
exports[`t-set t-set, t-if, and mix of expression/body lookup, 1 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-child-0/><block-child-0/><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
if (ctx['flag']) {
setContextValue(ctx, \\"ourvar\\", \`1\`);
} else {
setContextValue(ctx, \\"ourvar\\", 0);
}
let txt1 = ctx['ourvar'];
return block1([txt1]);
}
}"
`;
exports[`t-set t-set, t-if, and mix of expression/body lookup, 2 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-child-0/><block-child-0/><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
if (ctx['flag']) {
setContextValue(ctx, \\"ourvar\\", 1);
} else {
setContextValue(ctx, \\"ourvar\\", \`0\`);
}
let txt1 = ctx['ourvar'];
return block1([txt1]);
}
}"
`;
exports[`t-set t-set, t-if, and mix of expression/body lookup, 3 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
let b2;
if (ctx['flag']) {
setContextValue(ctx, \\"ourvar\\", 1);
} else {
setContextValue(ctx, \\"ourvar\\", \`0\`);
}
b2 = text(ctx['ourvar']);
return multi([b2]);
}
}"
`;
exports[`t-set value priority (with non text body 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, LazyValue } = helpers;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
let block2 = createBlock(\`<span>2</span>\`);
function value1(ctx, node, key = \\"\\") {
return block2();
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
ctx[\`value\`] = withDefault(1, new LazyValue(value1, ctx, node));
let txt1 = ctx['value'];
return block1([txt1]);
}
}"
`;
exports[`t-set value priority 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"value\\", withDefault(1, \`2\`));
let txt1 = ctx['value'];
return block1([txt1]);
}
}"
`;
@@ -1,120 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`qweb t-tag can fallback if falsy tag 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = tag => createBlock(\`<\${tag || 'fallback'}/>\`);
return function template(ctx, node, key = \\"\\") {
let tag1 = ctx['tag'];
return toggler(tag1, block1(tag1)());
}
}"
`;
exports[`qweb t-tag can update 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = tag => createBlock(\`<\${tag || 't'}/>\`);
return function template(ctx, node, key = \\"\\") {
let tag1 = ctx['tag'];
return toggler(tag1, block1(tag1)());
}
}"
`;
exports[`qweb t-tag simple usecases 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = tag => createBlock(\`<\${tag || 't'}/>\`);
return function template(ctx, node, key = \\"\\") {
let tag1 = 'div';
return toggler(tag1, block1(tag1)());
}
}"
`;
exports[`qweb t-tag simple usecases 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = tag => createBlock(\`<\${tag || 't'}>text</\${tag || 't'}>\`);
return function template(ctx, node, key = \\"\\") {
let tag1 = ctx['tag'];
return toggler(tag1, block1(tag1)());
}
}"
`;
exports[`qweb t-tag with multiple attributes 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = tag => createBlock(\`<\${tag || 't'} class=\\"blueberry\\" taste=\\"raspberry\\">gooseberry</\${tag || 't'}>\`);
return function template(ctx, node, key = \\"\\") {
let tag1 = ctx['tag'];
return toggler(tag1, block1(tag1)());
}
}"
`;
exports[`qweb t-tag with multiple child nodes 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = tag => createBlock(\`<\${tag || 't'}> pear <span>apple</span> strawberry </\${tag || 't'}>\`);
return function template(ctx, node, key = \\"\\") {
let tag1 = ctx['tag'];
return toggler(tag1, block1(tag1)());
}
}"
`;
exports[`qweb t-tag with multiple t-tag in same template 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = tag => createBlock(\`<\${tag || 't'}><block-child-0/></\${tag || 't'}>\`);
let block2 = tag => createBlock(\`<\${tag || 't'}>baz</\${tag || 't'}>\`);
return function template(ctx, node, key = \\"\\") {
let tag1 = ctx['outer'];
let tag2 = ctx['inner'];
let b2 = toggler(tag2, block2(tag2)());
return toggler(tag1, block1(tag1)([], [b2]));
}
}"
`;
exports[`qweb t-tag with multiple t-tag in same template, part 2 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block2 = tag => createBlock(\`<\${tag || 't'}>bar</\${tag || 't'}>\`);
let block3 = tag => createBlock(\`<\${tag || 't'}>baz</\${tag || 't'}>\`);
return function template(ctx, node, key = \\"\\") {
let tag1 = ctx['brother'];
let b2 = toggler(tag1, block2(tag1)());
let tag2 = ctx['brother'];
let b3 = toggler(tag2, block3(tag2)());
return multi([b2, b3]);
}
}"
`;
@@ -1,91 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`loading templates can initialize qweb with a string 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div>jupiler</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`loading templates can initialize qweb with an XMLDocument 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div>jupiler</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`loading templates can load a few templates from a xml string 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`items\`);
let block1 = createBlock(\`<ul><block-child-0/></ul>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
`;
exports[`loading templates can load a few templates from a xml string 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block2 = createBlock(\`<li>ok</li>\`);
let block3 = createBlock(\`<li>foo</li>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = block2();
let b3 = block3();
return multi([b2, b3]);
}
}"
`;
exports[`loading templates can load a few templates from an XMLDocument 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`items\`);
let block1 = createBlock(\`<ul><block-child-0/></ul>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
`;
exports[`loading templates can load a few templates from an XMLDocument 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block2 = createBlock(\`<li>ok</li>\`);
let block3 = createBlock(\`<li>foo</li>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = block2();
let b3 = block3();
return multi([b2, b3]);
}
}"
`;
@@ -1,66 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`translation support can set translatable attributes 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div tomato=\\"word\\" potato=\\"mot\\" title=\\"word\\">text</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`translation support can translate node content 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div>mot</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`translation support does not translate node content if disabled 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><span>mot</span><span>word</span></div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`translation support some attributes are translated 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><p label=\\"mot\\">mot</p><p title=\\"mot\\">mot</p><p placeholder=\\"mot\\">mot</p><p alt=\\"mot\\">mot</p><p something=\\"word\\">mot</p></div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div> mot </div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
-42
View File
@@ -1,42 +0,0 @@
import { renderToString, snapshotEverything, TestContext } from "../helpers";
snapshotEverything();
describe("error handling", () => {
test("invalid xml", () => {
expect(() => renderToString("<div>")).toThrow("Invalid XML in template");
});
test("nice warning if no template with given name", () => {
const context = new TestContext();
expect(() => context.renderToString("invalidname")).toThrow("Missing template");
});
test("cannot add twice the same template", () => {
const context = new TestContext();
context.addTemplate("test", `<t></t>`);
expect(() => context.addTemplate("test", "<div/>", { allowDuplicate: true })).not.toThrow(
"already defined"
);
expect(() => context.addTemplate("test", "<div/>")).toThrow("already defined");
});
test("addTemplates throw if parser error", () => {
const context = new TestContext();
expect(() => {
context.addTemplates("<templates><abc>></templates>");
}).toThrow("Invalid XML in template");
});
test("nice error when t-on is evaluated with a missing event", () => {
expect(() => renderToString(`<div t-on="somemethod"></div>`)).toThrow(
"Missing event name with t-on directive"
);
});
test("error when unknown directive", () => {
expect(() => renderToString(`<div t-best-beer="rochefort 10">test</div>`)).toThrow(
"Unknown QWeb directive: 't-best-beer'"
);
});
});
-567
View File
@@ -1,567 +0,0 @@
import { TemplateSet } from "../../src/app/template_set";
import { mount } from "../../src/blockdom";
import { makeTestFixture, renderToBdom, renderToString, snapshotEverything } from "../helpers";
import { markup } from "../../src/utils";
snapshotEverything();
// -----------------------------------------------------------------------------
// t-on
// -----------------------------------------------------------------------------
describe("t-on", () => {
function mountToFixture(template: string, ctx: any = {}, node?: any): HTMLDivElement {
if (!node) {
node = { component: ctx };
ctx.__owl__ = node;
}
const block = renderToBdom(template, ctx, node);
const fixture = makeTestFixture();
mount(block, fixture);
return fixture;
}
test("can bind event handler", () => {
const template = `<button t-on-click="add">Click</button>`;
let a = 1;
const fixture = mountToFixture(template, { add: () => (a = 3) });
expect(a).toBe(1);
fixture.querySelector("button")!.click();
expect(a).toBe(3);
});
test("receive event in first argument", () => {
expect.assertions(2);
const template = `<button t-on-click="add">Click</button>`;
const fixture = mountToFixture(template, {
add: (ev: any) => {
expect(ev).toBeInstanceOf(Event);
},
});
fixture.querySelector("button")!.click();
});
test("can bind two event handlers", () => {
const template = `
<button t-on-click="handleClick" t-on-dblclick="handleDblClick">Click</button>`;
let steps: string[] = [];
const fixture = mountToFixture(template, {
handleClick() {
steps.push("click");
},
handleDblClick() {
steps.push("dblclick");
},
});
expect(steps).toEqual([]);
fixture.querySelector("button")!.click();
expect(steps).toEqual(["click"]);
fixture.querySelector("button")!.dispatchEvent(new Event("dblclick", { bubbles: true }));
expect(steps).toEqual(["click", "dblclick"]);
});
test("can bind handlers with arguments", () => {
const template = `<button t-on-click="() => add(5)">Click</button>`;
let a = 1;
const fixture = mountToFixture(template, { add: (n: number) => (a = a + n) });
expect(a).toBe(1);
fixture.querySelector("button")!.click();
expect(a).toBe(6);
});
test("can bind handlers with object arguments", () => {
const template = `<button t-on-click="() => add({val: 5})">Click</button>`;
let a = 1;
const fixture = mountToFixture(template, { add: ({ val }: any) => (a = a + val) });
expect(a).toBe(1);
fixture.querySelector("button")!.click();
expect(a).toBe(6);
});
test("can bind handlers with empty object", () => {
expect.assertions(2);
const template = `<button t-on-click="() => doSomething({})">Click</button>`;
const fixture = mountToFixture(template, {
doSomething(arg: any) {
expect(arg).toEqual({});
},
});
fixture.querySelector("button")!.click();
});
test("can bind handlers with empty object (with non empty inner string)", () => {
expect.assertions(2);
const template = `<button t-on-click="() => doSomething({ })">Click</button>`;
const fixture = mountToFixture(template, {
doSomething(arg: any) {
expect(arg).toEqual({});
},
});
fixture.querySelector("button")!.click();
});
test("can bind handlers with empty object (with non empty inner string)", () => {
expect.assertions(2);
const template = `
<ul>
<li t-foreach="['someval']" t-as="action" t-key="action_index">
<a t-on-click="() => activate(action)">link</a>
</li>
</ul>`;
const fixture = mountToFixture(template, {
activate(action: string) {
expect(action).toBe("someval");
},
});
fixture.querySelector("a")!.click();
});
test("handler is bound to proper owner", () => {
expect.assertions(2);
const template = `<button t-on-click="add">Click</button>`;
let owner = {
add() {
expect(this).toBe(owner);
},
};
const fixture = mountToFixture(template, owner);
fixture.querySelector("button")!.click();
});
test("handler is bound to proper owner, part 2", () => {
expect.assertions(2);
const template = `
<t t-foreach="[1]" t-as="value" t-key="value">
<button t-on-click="add">Click</button>
</t>`;
let owner = {
add() {
expect(this).toBe(owner);
},
};
const fixture = mountToFixture(template, owner);
fixture.querySelector("button")!.click();
});
test("handler is bound to proper owner, part 3", () => {
expect.assertions(3);
const context = new TemplateSet();
const sub = `<button t-on-click="add">Click</button>`;
const main = `<t t-call="sub"/>`;
context.addTemplate("sub", sub);
context.addTemplate("main", main);
let owner: any = {
add() {
expect(this).toBe(owner);
},
};
const node = { component: owner };
owner.__owl__ = node;
const fixture = makeTestFixture();
const render = context.getTemplate("main");
const bdom = render(owner, node);
mount(bdom, fixture);
fixture.querySelector("button")!.click();
});
test("handler is bound to proper owner, part 4", () => {
expect.assertions(3);
const context = new TemplateSet();
const sub = `<button t-on-click="add">Click</button>`;
const main = `
<t t-foreach="[1]" t-as="value" t-key="value">
<t t-call="sub"/>
</t>`;
context.addTemplate("sub", sub);
context.addTemplate("main", main);
let owner: any = {
add() {
expect(this).toBe(owner);
},
};
const node = { component: owner };
owner.__owl__ = node;
const fixture = makeTestFixture();
const render = context.getTemplate("main");
const bdom = render(owner, node);
mount(bdom, fixture);
fixture.querySelector("button")!.click();
});
test("t-on with inline statement", () => {
const template = `<button t-on-click="() => state.counter++">Click</button>`;
let owner = { state: { counter: 0 } };
const fixture = mountToFixture(template, owner);
expect(owner.state.counter).toBe(0);
fixture.querySelector("button")!.click();
expect(owner.state.counter).toBe(1);
});
test("t-on with inline statement (function call)", () => {
const template = `<button t-on-click="() => state.incrementCounter(2)">Click</button>`;
let owner = {
state: {
counter: 0,
incrementCounter: (inc: number) => {
owner.state.counter += inc;
},
},
};
const fixture = mountToFixture(template, owner);
expect(owner.state.counter).toBe(0);
fixture.querySelector("button")!.click();
expect(owner.state.counter).toBe(2);
});
test("t-on with inline statement, part 2", () => {
const template = `<button t-on-click="() => state.flag = !state.flag">Toggle</button>`;
let owner = {
state: {
flag: true,
},
};
const fixture = mountToFixture(template, owner);
expect(owner.state.flag).toBe(true);
fixture.querySelector("button")!.click();
expect(owner.state.flag).toBe(false);
fixture.querySelector("button")!.click();
expect(owner.state.flag).toBe(true);
});
test("t-on with inline statement, part 3", () => {
const template = `<button t-on-click="() => state.n = someFunction(3)">Toggle</button>`;
let owner = {
someFunction(n: number) {
return n + 1;
},
state: {
n: 11,
},
};
const fixture = mountToFixture(template, owner);
expect(owner.state.n).toBe(11);
fixture.querySelector("button")!.click();
expect(owner.state.n).toBe(4);
});
test("t-on with t-call", async () => {
expect.assertions(3);
const app = new TemplateSet();
const sub = `<p t-on-click="update">lucas</p>`;
const main = `<div><t t-call="sub"/></div>`;
app.addTemplate("sub", sub);
app.addTemplate("main", main);
let owner: any = {
update() {
expect(this).toBe(owner);
},
};
const node = { component: owner };
owner.__owl__ = node;
const fixture = makeTestFixture();
const render = app.getTemplate("main");
const bdom = render(owner, node);
mount(bdom, fixture);
fixture.querySelector("p")!.click();
});
test("t-on, with arguments and t-call", async () => {
expect.assertions(4);
const app = new TemplateSet();
const sub = `<p t-on-click="() => this.update(value)">lucas</p>`;
const main = `<div><t t-call="sub"/></div>`;
app.addTemplate("sub", sub);
app.addTemplate("main", main);
let owner: any = {
update(val: number) {
expect(this).toBe(owner);
expect(val).toBe(444);
},
value: 444,
};
const node = { component: owner };
owner.__owl__ = node;
const fixture = makeTestFixture();
const render = app.getTemplate("main");
const bdom = render.call(owner, owner, node);
mount(bdom, fixture);
fixture.querySelector("p")!.click();
});
test("nice error when t-on is evaluated with a missing event", () => {
const template = `<div t-on="somemethod"></div>`;
expect(() => renderToString(template, { someMethod() {} })).toThrow(
"Missing event name with t-on directive"
);
});
describe("t-on modifiers (native listener)", () => {
test("basic support for native listener", () => {
const template = `<div class="myClass" t-on-click="divClicked">
<button t-on-click="btnClicked">Button</button>
</div>`;
const steps: string[] = [];
const owner = {
divClicked(ev: Event) {
expect(ev.currentTarget).toBe(div);
steps.push("divClicked");
},
btnClicked(ev: Event) {
expect(ev.currentTarget).toBe(button);
steps.push("btnClicked");
},
};
const node = mountToFixture(template, owner);
const div = node.querySelector(".myClass");
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
button.click();
expect(steps).toEqual(["btnClicked", "divClicked"]);
});
test("t-on with prevent and/or stop modifiers", async () => {
expect.assertions(7);
const template = `<div>
<button t-on-click.prevent="onClickPrevented">Button 1</button>
<button t-on-click.stop="onClickStopped">Button 2</button>
<button t-on-click.prevent.stop="onClickPreventedAndStopped">Button 3</button>
</div>`;
let owner = {
onClickPrevented(e: Event) {
expect(e.defaultPrevented).toBe(true);
expect(e.cancelBubble).toBe(false);
},
onClickStopped(e: Event) {
expect(e.defaultPrevented).toBe(false);
expect(e.cancelBubble).toBe(true);
},
onClickPreventedAndStopped(e: Event) {
expect(e.defaultPrevented).toBe(true);
expect(e.cancelBubble).toBe(true);
},
};
const node = mountToFixture(template, owner);
const buttons = (<HTMLElement>node).getElementsByTagName("button");
buttons[0].click();
buttons[1].click();
buttons[2].click();
});
test("t-on with self modifier", async () => {
expect.assertions(2);
const template = `<div>
<button t-on-click="onClick"><span>Button</span></button>
<button t-on-click.self="onClickSelf"><span>Button</span></button>
</div>`;
let steps: string[] = [];
let owner = {
onClick(e: Event) {
steps.push("onClick");
},
onClickSelf(e: Event) {
steps.push("onClickSelf");
},
};
const node = mountToFixture(template, owner);
const buttons = (<HTMLElement>node).getElementsByTagName("button");
const spans = (<HTMLElement>node).getElementsByTagName("span");
spans[0].click();
spans[1].click();
buttons[0].click();
buttons[1].click();
expect(steps).toEqual(["onClick", "onClick", "onClickSelf"]);
});
test("t-on with self and prevent modifiers (order matters)", async () => {
expect.assertions(2);
const template = `<div>
<button t-on-click.self.prevent="onClick"><span>Button</span></button>
</div>`;
let steps: boolean[] = [];
let owner = {
onClick() {},
};
const node = mountToFixture(template, owner);
(<HTMLElement>node).addEventListener("click", function (e) {
steps.push(e.defaultPrevented);
});
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
const span = (<HTMLElement>node).getElementsByTagName("span")[0];
span.click();
button.click();
expect(steps).toEqual([false, true]);
});
test("t-on with prevent and self modifiers (order matters)", async () => {
expect.assertions(2);
const template = `<div>
<button t-on-click.prevent.self="onClick"><span>Button</span></button>
</div>`;
let steps: boolean[] = [];
let owner = {
onClick() {},
};
const node = mountToFixture(template, owner);
(<HTMLElement>node).addEventListener("click", function (e) {
steps.push(e.defaultPrevented);
});
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
const span = (<HTMLElement>node).getElementsByTagName("span")[0];
span.click();
button.click();
expect(steps).toEqual([true, true]);
});
test("t-on with prevent modifier in t-foreach", async () => {
expect.assertions(5);
const template = `<div>
<t t-foreach="projects" t-as="project" t-key="project">
<a href="#" t-on-click.prevent="ev => onEdit(project.id, ev)">
Edit <t t-esc="project.name"/>
</a>
</t>
</div>`;
const steps: string[] = [];
const owner = {
projects: [
{ id: 1, name: "Project 1" },
{ id: 2, name: "Project 2" },
],
onEdit(projectId: string, ev: Event) {
expect(ev.defaultPrevented).toBe(true);
steps.push(projectId);
},
};
const node = mountToFixture(template, owner);
expect(node.innerHTML).toBe(
`<div><a href="#"> Edit Project 1</a><a href="#"> Edit Project 2</a></div>`
);
const links = node.querySelectorAll("a")!;
links[0].click();
links[1].click();
expect(steps).toEqual([1, 2]);
});
test("t-on with empty handler (only modifiers)", () => {
expect.assertions(2);
const template = `<div>
<button t-on-click.prevent="">Button</button>
</div>`;
const node = mountToFixture(template, {});
node.addEventListener("click", (e) => {
expect(e.defaultPrevented).toBe(true);
});
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
button.click();
});
test("t-on combined with t-esc", async () => {
expect.assertions(3);
const template = `<div><button t-on-click="onClick" t-esc="text"/></div>`;
const steps: string[] = [];
const owner = {
text: "Click here",
onClick() {
steps.push("onClick");
},
};
const node = mountToFixture(template, owner);
expect(node.innerHTML).toBe(`<div><button>Click here</button></div>`);
node.querySelector("button")!.click();
expect(steps).toEqual(["onClick"]);
});
test("t-on combined with t-out", async () => {
expect.assertions(3);
const template = `<div><button t-on-click="onClick" t-out="html"/></div>`;
const steps: string[] = [];
const owner = {
html: markup("Click <b>here</b>"),
onClick() {
steps.push("onClick");
},
};
const node = mountToFixture(template, owner);
expect(node.innerHTML).toBe(`<div><button>Click <b>here</b></button></div>`);
node.querySelector("button")!.click();
expect(steps).toEqual(["onClick"]);
});
test("t-on with .capture modifier", () => {
expect.assertions(2);
const template = `<div t-on-click.capture="onCapture">
<button t-on-click="doSomething">Button</button>
</div>`;
const steps: string[] = [];
const owner = {
onCapture() {
steps.push("captured");
},
doSomething() {
steps.push("normal");
},
};
const node = mountToFixture(template, owner);
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
button.click();
expect(steps).toEqual(["captured", "normal"]);
});
});
describe("t-on modifiers (synthetic listener)", () => {
test("basic support for synthetic", () => {
const template = `<div t-on-click.synthetic="divClicked">
<button t-on-click.synthetic="btnClicked">Button</button>
</div>`;
const steps: string[] = [];
const owner = {
divClicked(ev: Event) {
expect(ev.currentTarget).toBe(document);
steps.push("divClicked");
},
btnClicked(ev: Event) {
expect(ev.currentTarget).toBe(document);
steps.push("btnClicked");
},
};
const node = mountToFixture(template, owner);
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
button.click();
expect(steps).toEqual(["btnClicked", "divClicked"]);
});
});
});
-12
View File
@@ -1,12 +0,0 @@
import { renderToString, snapshotEverything } from "../helpers";
snapshotEverything();
describe("memory", () => {
test("t-foreach does not leak stuff in global scope", () => {
const initialNumberOfGlobals = Object.keys(window).length;
const template = `<p><t t-foreach="[3, 2, 1]" t-as="item" t-key="item_index"><t t-esc="item"/></t></p>`;
expect(renderToString(template)).toBe("<p>321</p>");
expect(Object.keys(window).length).toBe(initialNumberOfGlobals);
});
});
-30
View File
@@ -1,30 +0,0 @@
import { renderToString, snapshotEverything } from "../helpers";
// NB: check the snapshots to see where the SVG namespaces are added
snapshotEverything();
describe("properly support svg", () => {
test("add proper namespace to svg", () => {
const template = `<svg width="100px" height="90px"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </svg>`;
expect(renderToString(template)).toBe(
`<svg width=\"100px\" height=\"90px\"><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </svg>`
);
});
test("add proper namespace to g tags", () => {
const template = `<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </g>`;
expect(renderToString(template)).toBe(
`<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
);
});
test("namespace to g tags not added if already in svg namespace", () => {
const template = `<svg><g/></svg>`;
expect(renderToString(template)).toBe(`<svg><g></g></svg>`);
});
test("namespace to svg tags added even if already in svg namespace", () => {
const template = `<svg><svg/></svg>`;
expect(renderToString(template)).toBe(`<svg><svg></svg></svg>`);
});
});
-46
View File
@@ -1,46 +0,0 @@
import { renderToString, renderToBdom, snapshotEverything, makeTestFixture } from "../helpers";
import { mount, patch } from "../../src/blockdom/index";
snapshotEverything();
describe("t-key", () => {
test("can use t-key directive on a node", () => {
const template = `<div t-key="beer.id"><t t-esc="beer.name"/></div>`;
expect(renderToString(template, { beer: { id: 12, name: "Chimay Rouge" } })).toBe(
"<div>Chimay Rouge</div>"
);
});
test("can use t-key directive on a node as a function", () => {
const template = `<div t-key="getKey(beer)"><t t-esc="beer.name"/></div>`;
const getKey = (arg: any) => arg.id;
expect(renderToString(template, { getKey, beer: { id: 12, name: "Chimay Rouge" } })).toBe(
"<div>Chimay Rouge</div>"
);
});
test("can use t-key directive on a node 2", async () => {
const template = `<div t-key="beer.id"><t t-esc="beer.name"/></div>`;
const bd = renderToBdom(template, { beer: { id: 12, name: "Chimay Rouge" } });
const fixture = makeTestFixture();
await mount(bd, fixture);
const div = fixture.firstChild;
expect((div as HTMLElement).outerHTML).toBe("<div>Chimay Rouge</div>");
const bd2 = renderToBdom(template, { beer: { id: 13, name: "Chimay Rouge" } });
await patch(bd, bd2);
expect(div !== fixture.firstChild).toBeTruthy();
expect((div as HTMLElement).outerHTML).toBe("<div>Chimay Rouge</div>");
});
test("t-key directive in a list", () => {
const template = `<ul>
<li t-foreach="beers" t-as="beer" t-key="beer.id"><t t-esc="beer.name"/></li>
</ul>`;
expect(
renderToString(template, {
beers: [{ id: 12, name: "Chimay Rouge" }],
})
).toBe("<ul><li>Chimay Rouge</li></ul>");
});
});
-239
View File
@@ -1,239 +0,0 @@
import {
renderToString,
renderToBdom,
snapshotEverything,
TestContext,
makeTestFixture,
} from "../helpers";
import { mount, patch } from "../../src/blockdom";
import { createBlock } from "../../src/blockdom/index";
import { markup } from "../../src/utils";
snapshotEverything();
// -----------------------------------------------------------------------------
// t-out
// -----------------------------------------------------------------------------
describe("t-out", () => {
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
test("literal", () => {
const template = `<span><t t-out="'ok'"/></span>`;
expect(renderToString(template)).toBe("<span>ok</span>");
});
test("literal, no outside html element", () => {
const template = `<t t-out="'ok'"/>`;
expect(renderToString(template)).toBe("ok");
});
test("variable", () => {
const template = `<span><t t-out="var"/></span>`;
expect(renderToString(template, { var: "ok" })).toBe("<span>ok</span>");
});
test("not escaping", () => {
const template = `<div><t t-out="var"/></div>`;
expect(renderToString(template, { var: markup("<ok></ok>") })).toBe("<div><ok></ok></div>");
});
test("t-out and another sibling node", () => {
const template = `<span><span>hello</span><t t-out="var"/></span>`;
expect(renderToString(template, { var: markup("<ok>world</ok>") })).toBe(
"<span><span>hello</span><ok>world</ok></span>"
);
});
test("t-out with comment", () => {
const template = `<span><t t-out="var"/></span>`;
expect(renderToString(template, { var: markup("<p>text<!-- top secret --></p>") })).toBe(
"<span><p>text<!-- top secret --></p></span>"
);
});
test("t-out on a node with a body, as a default", () => {
const template = `<span t-out="var">nope</span>`;
expect(renderToString(template)).toBe("<span>nope</span>");
});
test("t-out with a <t/> in body", () => {
const template = `<t t-out="var"><t></t></t>`;
expect(renderToString(template, { var: "coucou" })).toBe("coucou");
});
test("t-out with just a t-set t-value in body", () => {
const template = `<t t-out="var"><t t-set="a" t-value="1" /></t>`;
expect(renderToString(template, { var: "coucou" })).toBe("coucou");
});
test("t-out on a node with a dom node in body, as a default", () => {
const template = `<span t-out="var"><div>nope</div></span>`;
expect(renderToString(template)).toBe("<span><div>nope</div></span>");
});
test("multiple calls to t-out", () => {
const context = new TestContext();
const sub = `
<div>
<t t-out="0"/>
<div>Greeter</div>
<t t-out="0"/>
</div>`;
const main = `
<div>
<t t-call="sub">
<span>coucou</span>
</t>
</div>`;
context.addTemplate("sub", sub);
context.addTemplate("main", main);
const expected =
"<div><div><span>coucou</span><div>Greeter</div><span>coucou</span></div></div>";
expect(context.renderToString("main")).toBe(expected);
});
test("t-out escaped", () => {
const template = `<span t-out="var" />`;
expect(renderToString(template, { var: "<div>nope</div>" })).toBe(
"<span>&lt;div&gt;nope&lt;/div&gt;</span>"
);
});
test("t-out markedup", () => {
const template = `<span t-out="var" />`;
expect(renderToString(template, { var: markup("<div>nope</div>") })).toBe(
"<span><div>nope</div></span>"
);
});
test("t-out switch escaped", () => {
const template = `<span t-out="var" />`;
const node = renderToBdom(template, { var: "<div>nope</div>" });
mount(node, fixture);
expect(fixture.innerHTML).toBe("<span>&lt;div&gt;nope&lt;/div&gt;</span>");
patch(node, renderToBdom(template, { var: "<li>yep</li>" }));
expect(fixture.innerHTML).toBe("<span>&lt;li&gt;yep&lt;/li&gt;</span>");
});
test("t-out switch markup", () => {
const template = `<span t-out="var" />`;
const node = renderToBdom(template, { var: markup("<div>nope</div>") });
mount(node, fixture);
expect(fixture.innerHTML).toBe("<span><div>nope</div></span>");
patch(node, renderToBdom(template, { var: markup("<li>yep</li>") }));
expect(fixture.innerHTML).toBe("<span><li>yep</li></span>");
});
test("t-out switch markup on escaped", () => {
const template = `<span t-out="var" />`;
const node = renderToBdom(template, { var: "<div>nope</div>" });
mount(node, fixture);
expect(fixture.innerHTML).toBe("<span>&lt;div&gt;nope&lt;/div&gt;</span>");
patch(node, renderToBdom(template, { var: markup("<li>yep</li>") }));
expect(fixture.innerHTML).toBe("<span><li>yep</li></span>");
});
test("t-out switch escaped on markup", () => {
const template = `<span t-out="var" />`;
const node = renderToBdom(template, { var: markup("<div>nope</div>") });
mount(node, fixture);
expect(fixture.innerHTML).toBe("<span><div>nope</div></span>");
patch(node, renderToBdom(template, { var: "<li>yep</li>" }));
expect(fixture.innerHTML).toBe("<span>&lt;li&gt;yep&lt;/li&gt;</span>");
});
test("t-out block", () => {
const block = createBlock("<div>block</div>");
const template = `<span t-out="bdom"/>`;
expect(renderToString(template, { bdom: block() })).toBe("<span><div>block</div></span>");
});
test("t-out bdom", () => {
const template = `<div><t t-set="var"><ol>set</ol></t><span t-out="var" /></div>`;
expect(renderToString(template)).toBe("<div><span><ol>set</ol></span></div>");
});
test("t-out switch markup on bdom", () => {
const template = `<div>
<t t-set="bdom"><ol>set</ol></t>
<t t-if="hasBdom">
<span t-out="bdom" />
</t>
<t t-else="">
<span t-out="var" />
</t>
</div>`;
const node = renderToBdom(template, { hasBdom: true, var: markup("<li>yep</li>") });
mount(node, fixture);
expect(fixture.innerHTML).toBe("<div><span><ol>set</ol></span></div>");
patch(node, renderToBdom(template, { hasBdom: false, var: markup("<li>yep</li>") }));
expect(fixture.innerHTML).toBe("<div><span><li>yep</li></span></div>");
});
test("t-out 0", () => {
const context = new TestContext();
context.addTemplate("_basic-callee", `<div><t t-out="0" /></div>`);
context.addTemplate("caller", `<div><t t-call="_basic-callee"><div>zero</div></t></div>`);
const node = context.getTemplate("caller")({}, null, "");
mount(node, fixture);
expect(fixture.innerHTML).toBe("<div><div><div>zero</div></div></div>");
});
test("t-out with arbitrary object", () => {
const template = `<div t-out="var" />`;
const node = renderToBdom(template, { var: { someKey: "someValue" } });
expect(() => mount(node, fixture)).toThrow();
});
test("t-out with arbitrary object 2", () => {
const template = `<div t-out="var" />`;
const node = renderToBdom(template, { var: ["someValue"] });
expect(() => mount(node, fixture)).toThrow();
});
});
describe("t-raw is deprecated", () => {
test("should warn", () => {
const template = `<div t-raw="var" />`;
const warn = console.warn;
const steps: string[] = [];
console.warn = (msg: any) => steps.push(msg);
expect(renderToString(template, { var: "<div>escaped</div>" })).toBe(
"<div>&lt;div&gt;escaped&lt;/div&gt;</div>"
);
expect(steps).toEqual([
't-raw has been deprecated in favor of t-out. If the value to render is not wrapped by the "markup" function, it will be escaped',
]);
console.warn = warn;
});
test("t-out is actually called in t-raw's place", () => {
const template = `<div t-raw="var" />`;
const warn = console.warn;
console.warn = (msg: any) => msg;
expect(renderToString(template, { var: markup("<div>raw</div>") })).toBe(
"<div><div>raw</div></div>"
);
console.warn = warn;
});
});
-69
View File
@@ -1,69 +0,0 @@
import { mount, patch } from "../../src/blockdom";
import { makeTestFixture, renderToBdom, renderToString, snapshotEverything } from "../helpers";
snapshotEverything();
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
describe("qweb t-tag", () => {
test("simple usecases", () => {
expect(renderToString(`<t t-tag="'div'"></t>`)).toBe("<div></div>");
expect(renderToString(`<t t-tag="tag">text</t>`, { tag: "span" })).toBe("<span>text</span>");
});
test("with multiple child nodes", () => {
const template = `
<t t-tag="tag">
pear
<span>apple</span>
strawberry
</t>`;
expect(renderToString(template, { tag: "div" })).toBe(
"<div> pear <span>apple</span> strawberry </div>"
);
});
test("with multiple attributes", () => {
const template = `<t t-tag="tag" class="blueberry" taste="raspberry">gooseberry</t>`;
const expected = `<div class=\"blueberry\" taste=\"raspberry\">gooseberry</div>`;
expect(renderToString(template, { tag: "div" })).toBe(expected);
});
test("can fallback if falsy tag", () => {
const template = `<fallback t-tag="tag"/>`;
expect(renderToString(template, { tag: "div" })).toBe(`<div></div>`);
expect(renderToString(template, { tag: "" })).toBe(`<fallback></fallback>`);
expect(renderToString(template, { tag: undefined })).toBe(`<fallback></fallback>`);
expect(renderToString(template, { tag: null })).toBe(`<fallback></fallback>`);
expect(renderToString(template, { tag: false })).toBe(`<fallback></fallback>`);
});
test("with multiple t-tag in same template", () => {
const template = `<t t-tag="outer"><t t-tag="inner">baz</t></t>`;
expect(renderToString(template, { outer: "foo", inner: "bar" })).toBe(
`<foo><bar>baz</bar></foo>`
);
});
test("with multiple t-tag in same template, part 2", () => {
const template = `<t t-tag="brother">bar</t><t t-tag="brother">baz</t>`;
expect(renderToString(template, { brother: "foo" })).toBe(`<foo>bar</foo><foo>baz</foo>`);
});
test("can update", () => {
const template = `<t t-tag="tag"></t>`;
const bdom = renderToBdom(template, { tag: "yop" });
mount(bdom, fixture);
expect(fixture.innerHTML).toBe("<yop></yop>");
patch(bdom, renderToBdom(template, { tag: "gnap" }));
expect(fixture.innerHTML).toBe("<gnap></gnap>");
});
});
-70
View File
@@ -1,70 +0,0 @@
import { snapshotEverything, TestContext } from "../helpers";
snapshotEverything();
describe("loading templates", () => {
test("can initialize qweb with a string", () => {
const templates = `<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<div t-name="hey">jupiler</div>
</templates>`;
const context = new TestContext();
context.addTemplates(templates);
expect(context.renderToString("hey")).toBe("<div>jupiler</div>");
});
test("can initialize qweb with an XMLDocument", () => {
const data = `<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<div t-name="hey">jupiler</div>
</templates>`;
const xml = new DOMParser().parseFromString(data, "text/xml");
const context = new TestContext();
context.addTemplates(xml);
expect(context.renderToString("hey")).toBe("<div>jupiler</div>");
});
test("can load a few templates from a xml string", () => {
const data = `<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="items"><li>ok</li><li>foo</li></t>
<ul t-name="main"><t t-call="items"/></ul>
</templates>`;
const context = new TestContext();
context.addTemplates(data);
const result = context.renderToString("main");
expect(result).toBe("<ul><li>ok</li><li>foo</li></ul>");
});
test("can load a few templates from an XMLDocument", () => {
const data = `<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="items"><li>ok</li><li>foo</li></t>
<ul t-name="main"><t t-call="items"/></ul>
</templates>`;
const xml = new DOMParser().parseFromString(data, "text/xml");
const context = new TestContext();
context.addTemplates(xml);
const result = context.renderToString("main");
expect(result).toBe("<ul><li>ok</li><li>foo</li></ul>");
});
test("does not crash if string does not have templates", () => {
const data = "";
const context = new TestContext();
context.addTemplates(data);
expect(Object.keys(context.rawTemplates)).toEqual([]);
});
test("does not crash if XMLDocument does not have templates", () => {
const data = "";
const xml = new DOMParser().parseFromString(data, "text/xml");
const context = new TestContext();
context.addTemplates(xml);
expect(Object.keys(context.rawTemplates)).toEqual([]);
});
});
-89
View File
@@ -1,89 +0,0 @@
import { Component, mount, xml } from "../../src";
import { makeTestFixture, snapshotEverything } from "../helpers";
let fixture: HTMLElement;
snapshotEverything();
beforeEach(() => {
fixture = makeTestFixture();
});
describe("translation support", () => {
test("can translate node content", async () => {
class SomeComponent extends Component {
static template = xml`<div>word</div>`;
}
await mount(SomeComponent, fixture, {
translateFn: (expr: string) => (expr === "word" ? "mot" : expr),
});
expect(fixture.innerHTML).toBe("<div>mot</div>");
});
test("does not translate node content if disabled", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<span>word</span>
<span t-translation="off">word</span>
</div>
`;
}
await mount(SomeComponent, fixture, {
translateFn: (expr: string) => (expr === "word" ? "mot" : expr),
});
expect(fixture.innerHTML).toBe("<div><span>mot</span><span>word</span></div>");
});
test("some attributes are translated", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<p label="word">word</p>
<p title="word">word</p>
<p placeholder="word">word</p>
<p alt="word">word</p>
<p something="word">word</p>
</div>
`;
}
await mount(SomeComponent, fixture, {
translateFn: (expr: string) => (expr === "word" ? "mot" : expr),
});
expect(fixture.innerHTML).toBe(
'<div><p label="mot">mot</p><p title="mot">mot</p><p placeholder="mot">mot</p><p alt="mot">mot</p><p something="word">mot</p></div>'
);
});
test("can set translatable attributes", async () => {
class SomeComponent extends Component {
static template = xml`
<div tomato="word" potato="word" title="word">text</div>
`;
}
await mount(SomeComponent, fixture, {
translateFn: (expr: string) => (expr === "word" ? "mot" : expr),
translatableAttributes: ["potato"],
});
expect(fixture.innerHTML).toBe('<div tomato="word" potato="mot" title="word">text</div>');
});
test("translation is done on the trimmed text, with extra spaces readded after", async () => {
class SomeComponent extends Component {
static template = xml`
<div> word </div>
`;
}
const translateFn = jest.fn((expr: string) => (expr === "word" ? "mot" : expr));
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("<div> mot </div>");
expect(translateFn).toHaveBeenCalledWith("word");
});
});
@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app destroy remove the widget from the DOM 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More